bootstrap-sass 2.3.1.3 → 3.0.0.0

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

Potentially problematic release.


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

Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -0
  3. data/.travis.yml +12 -0
  4. data/CHANGELOG.md +104 -0
  5. data/CONTRIBUTING.md +79 -0
  6. data/Gemfile +3 -0
  7. data/README.md +114 -78
  8. data/Rakefile +48 -0
  9. data/bootstrap-sass.gemspec +28 -0
  10. data/lib/bootstrap-sass/engine.rb +4 -2
  11. data/lib/bootstrap-sass/version.rb +4 -0
  12. data/lib/bootstrap-sass.rb +10 -5
  13. data/tasks/converter.rb +829 -0
  14. data/templates/project/_variables.scss.erb +3 -0
  15. data/templates/project/manifest.rb +10 -13
  16. data/templates/project/styles.scss +1 -6
  17. data/test/compass_test.rb +8 -0
  18. data/test/compilation_test.rb +13 -0
  19. data/test/dummy/README.rdoc +3 -0
  20. data/test/dummy/Rakefile +6 -0
  21. data/test/dummy/app/assets/images/.keep +0 -0
  22. data/test/dummy/app/assets/javascripts/application.js +2 -0
  23. data/test/dummy/app/assets/stylesheets/application.css.sass +1 -0
  24. data/test/dummy/app/controllers/application_controller.rb +5 -0
  25. data/test/dummy/app/controllers/pages_controller.rb +4 -0
  26. data/test/dummy/app/helpers/application_helper.rb +2 -0
  27. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/test/dummy/app/views/pages/root.html.slim +11 -0
  29. data/test/dummy/bin/bundle +3 -0
  30. data/test/dummy/bin/rails +4 -0
  31. data/test/dummy/bin/rake +4 -0
  32. data/test/dummy/config/application.rb +17 -0
  33. data/test/dummy/config/boot.rb +5 -0
  34. data/test/dummy/config/environment.rb +5 -0
  35. data/test/dummy/config/environments/development.rb +26 -0
  36. data/test/dummy/config/environments/production.rb +76 -0
  37. data/test/dummy/config/environments/test.rb +30 -0
  38. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  39. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  40. data/test/dummy/config/initializers/inflections.rb +16 -0
  41. data/test/dummy/config/initializers/mime_types.rb +5 -0
  42. data/test/dummy/config/initializers/secret_token.rb +18 -0
  43. data/test/dummy/config/initializers/session_store.rb +3 -0
  44. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  45. data/test/dummy/config/locales/en.yml +3 -0
  46. data/test/dummy/config/locales/es.yml +3 -0
  47. data/test/dummy/config/routes.rb +3 -0
  48. data/test/dummy/config.ru +4 -0
  49. data/test/dummy/db/test.sqlite3 +0 -0
  50. data/test/dummy/lib/assets/.keep +0 -0
  51. data/test/dummy/log/.keep +0 -0
  52. data/test/dummy/log/development.log +0 -0
  53. data/test/dummy/public/404.html +58 -0
  54. data/test/dummy/public/422.html +58 -0
  55. data/test/dummy/public/500.html +57 -0
  56. data/test/dummy/public/favicon.ico +0 -0
  57. data/test/gemfiles/sass_3_2.gemfile +5 -0
  58. data/test/gemfiles/sass_head.gemfile +6 -0
  59. data/test/pages_test.rb +14 -0
  60. data/test/support/integration_test.rb +29 -0
  61. data/test/test_helper.rb +32 -0
  62. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
  63. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +228 -0
  64. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  65. data/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
  66. data/vendor/assets/javascripts/bootstrap/affix.js +126 -0
  67. data/vendor/assets/javascripts/bootstrap/alert.js +98 -0
  68. data/vendor/assets/javascripts/bootstrap/button.js +109 -0
  69. data/vendor/assets/javascripts/bootstrap/carousel.js +217 -0
  70. data/vendor/assets/javascripts/bootstrap/collapse.js +179 -0
  71. data/vendor/assets/javascripts/bootstrap/dropdown.js +154 -0
  72. data/vendor/assets/javascripts/bootstrap/modal.js +246 -0
  73. data/vendor/assets/javascripts/bootstrap/popover.js +117 -0
  74. data/vendor/assets/javascripts/bootstrap/scrollspy.js +158 -0
  75. data/vendor/assets/javascripts/bootstrap/tab.js +135 -0
  76. data/vendor/assets/javascripts/bootstrap/tooltip.js +386 -0
  77. data/vendor/assets/javascripts/bootstrap/transition.js +56 -0
  78. data/vendor/assets/javascripts/bootstrap.js +12 -13
  79. data/vendor/assets/stylesheets/bootstrap/_alerts.scss +46 -58
  80. data/vendor/assets/stylesheets/bootstrap/_badges.scss +51 -0
  81. data/vendor/assets/stylesheets/bootstrap/_breadcrumbs.scss +8 -9
  82. data/vendor/assets/stylesheets/bootstrap/_button-groups.scss +173 -154
  83. data/vendor/assets/stylesheets/bootstrap/_buttons.scss +97 -165
  84. data/vendor/assets/stylesheets/bootstrap/_carousel.scss +116 -65
  85. data/vendor/assets/stylesheets/bootstrap/_close.scss +11 -8
  86. data/vendor/assets/stylesheets/bootstrap/_code.scss +16 -21
  87. data/vendor/assets/stylesheets/bootstrap/_component-animations.scss +10 -3
  88. data/vendor/assets/stylesheets/bootstrap/_dropdowns.scss +103 -146
  89. data/vendor/assets/stylesheets/bootstrap/_forms.scss +222 -559
  90. data/vendor/assets/stylesheets/bootstrap/_glyphicons.scss +232 -0
  91. data/vendor/assets/stylesheets/bootstrap/_grid.scss +336 -11
  92. data/vendor/assets/stylesheets/bootstrap/_input-groups.scss +127 -0
  93. data/vendor/assets/stylesheets/bootstrap/_jumbotron.scss +40 -0
  94. data/vendor/assets/stylesheets/bootstrap/_labels.scss +58 -0
  95. data/vendor/assets/stylesheets/bootstrap/_list-group.scss +90 -0
  96. data/vendor/assets/stylesheets/bootstrap/_media.scss +8 -7
  97. data/vendor/assets/stylesheets/bootstrap/_mixins.scss +470 -430
  98. data/vendor/assets/stylesheets/bootstrap/_modals.scss +103 -52
  99. data/vendor/assets/stylesheets/bootstrap/_navbar.scss +511 -383
  100. data/vendor/assets/stylesheets/bootstrap/_navs.scss +169 -349
  101. data/vendor/assets/stylesheets/bootstrap/_normalize.scss +396 -0
  102. data/vendor/assets/stylesheets/bootstrap/_pager.scss +45 -33
  103. data/vendor/assets/stylesheets/bootstrap/_pagination.scss +65 -105
  104. data/vendor/assets/stylesheets/bootstrap/_panels.scss +148 -0
  105. data/vendor/assets/stylesheets/bootstrap/_popovers.scss +51 -51
  106. data/vendor/assets/stylesheets/bootstrap/_print.scss +100 -0
  107. data/vendor/assets/stylesheets/bootstrap/_progress-bars.scss +28 -55
  108. data/vendor/assets/stylesheets/bootstrap/_responsive-utilities.scss +180 -45
  109. data/vendor/assets/stylesheets/bootstrap/_scaffolding.scss +101 -24
  110. data/vendor/assets/stylesheets/bootstrap/_tables.scss +169 -168
  111. data/vendor/assets/stylesheets/bootstrap/_theme.scss +232 -0
  112. data/vendor/assets/stylesheets/bootstrap/_thumbnails.scss +11 -33
  113. data/vendor/assets/stylesheets/bootstrap/_tooltip.scss +45 -20
  114. data/vendor/assets/stylesheets/bootstrap/_type.scss +101 -110
  115. data/vendor/assets/stylesheets/bootstrap/_utilities.scss +19 -22
  116. data/vendor/assets/stylesheets/bootstrap/_variables.scss +498 -179
  117. data/vendor/assets/stylesheets/bootstrap/_wells.scss +7 -7
  118. data/vendor/assets/stylesheets/bootstrap/bootstrap.scss +29 -33
  119. metadata +226 -44
  120. data/templates/project/_variables.scss +0 -301
  121. data/vendor/assets/images/glyphicons-halflings-white.png +0 -0
  122. data/vendor/assets/images/glyphicons-halflings.png +0 -0
  123. data/vendor/assets/javascripts/bootstrap-affix.js +0 -117
  124. data/vendor/assets/javascripts/bootstrap-alert.js +0 -99
  125. data/vendor/assets/javascripts/bootstrap-button.js +0 -105
  126. data/vendor/assets/javascripts/bootstrap-carousel.js +0 -207
  127. data/vendor/assets/javascripts/bootstrap-collapse.js +0 -167
  128. data/vendor/assets/javascripts/bootstrap-dropdown.js +0 -165
  129. data/vendor/assets/javascripts/bootstrap-modal.js +0 -247
  130. data/vendor/assets/javascripts/bootstrap-popover.js +0 -114
  131. data/vendor/assets/javascripts/bootstrap-scrollspy.js +0 -162
  132. data/vendor/assets/javascripts/bootstrap-tab.js +0 -144
  133. data/vendor/assets/javascripts/bootstrap-tooltip.js +0 -361
  134. data/vendor/assets/javascripts/bootstrap-transition.js +0 -60
  135. data/vendor/assets/javascripts/bootstrap-typeahead.js +0 -335
  136. data/vendor/assets/stylesheets/bootstrap/_accordion.scss +0 -34
  137. data/vendor/assets/stylesheets/bootstrap/_hero-unit.scss +0 -25
  138. data/vendor/assets/stylesheets/bootstrap/_labels-badges.scss +0 -83
  139. data/vendor/assets/stylesheets/bootstrap/_layouts.scss +0 -16
  140. data/vendor/assets/stylesheets/bootstrap/_reset.scss +0 -216
  141. data/vendor/assets/stylesheets/bootstrap/_responsive-1200px-min.scss +0 -28
  142. data/vendor/assets/stylesheets/bootstrap/_responsive-767px-max.scss +0 -193
  143. data/vendor/assets/stylesheets/bootstrap/_responsive-768px-979px.scss +0 -19
  144. data/vendor/assets/stylesheets/bootstrap/_responsive-navbar.scss +0 -189
  145. data/vendor/assets/stylesheets/bootstrap/_sprites.scss +0 -197
  146. data/vendor/assets/stylesheets/bootstrap/responsive.scss +0 -48
  147. data/vendor/assets/stylesheets/bootstrap-responsive.scss +0 -1
@@ -0,0 +1,829 @@
1
+ # coding: utf-8
2
+ # Based on convert script from vwall/compass-twitter-bootstrap gem.
3
+ # https://github.com/vwall/compass-twitter-bootstrap/blob/master/build/convert.rb
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this work except in compliance with the License.
7
+ # You may obtain a copy of the License in the LICENSE file, or at:
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'open-uri'
18
+ require 'json'
19
+ require 'strscan'
20
+ require 'forwardable'
21
+ require 'term/ansicolor'
22
+ require 'fileutils'
23
+
24
+ class Converter
25
+ extend Forwardable
26
+
27
+ GIT_DATA = 'https://api.github.com/repos'
28
+ GIT_RAW = 'https://raw.github.com'
29
+
30
+ def initialize(branch)
31
+ @repo = 'twbs/bootstrap'
32
+ @repo_url = "https://github.com/#@repo"
33
+ @branch = branch || 'master'
34
+ @branch_sha = get_branch_sha
35
+ @save_at = { js: 'vendor/assets/javascripts/bootstrap',
36
+ scss: 'vendor/assets/stylesheets/bootstrap',
37
+ fonts: 'vendor/assets/fonts/bootstrap' }
38
+ @save_at.each { |_,v| FileUtils.mkdir_p(v) }
39
+ @cache_path = 'tmp/converter-cache'
40
+ @logger = Logger.new(repo: @repo_url, branch: @branch, branch_sha: @branch_sha, save_at: @save_at, cache_path: @cache_path)
41
+ end
42
+
43
+ def_delegators :@logger, :log_status, :log_processing, :log_transform, :log_file_info, :log_processed, :log_http_get_file, :log_http_get_files, :silence_log
44
+
45
+ def process
46
+ process_stylesheet_assets
47
+ process_javascript_assets
48
+ process_font_assets
49
+ store_version
50
+ end
51
+
52
+ def process_font_assets
53
+ log_status "Processing fonts..."
54
+ files = read_files('fonts', bootstrap_font_files)
55
+ save_at = @save_at[:fonts]
56
+ files.each do |name, content|
57
+ save_file "#{save_at}/#{name}", content
58
+ end
59
+ end
60
+
61
+ NESTED_MIXINS = {'#gradient' => 'gradient'}
62
+ VARARG_MIXINS = %w(transition transition-transform box-shadow)
63
+ def process_stylesheet_assets
64
+ log_status "Processing stylesheets..."
65
+ files = read_files('less', bootstrap_less_files)
66
+
67
+ # read common mixin definitions (incl. nested mixins) from mixins.less
68
+ read_shared_mixins! files['mixins.less']
69
+
70
+ # convert each file
71
+ files.each do |name, file|
72
+ log_processing name
73
+ # apply common conversions
74
+ file = convert_to_scss(file)
75
+ case name
76
+ when 'mixins.less'
77
+ NESTED_MIXINS.each do |selector, prefix|
78
+ file = flatten_mixins(file, selector, prefix)
79
+ end
80
+ file = varargify_mixin_definitions(file, *VARARG_MIXINS)
81
+ file = deinterpolate_vararg_mixins(file)
82
+ file = parameterize_mixin_parent_selector file, 'responsive-(in)?visibility'
83
+ file = parameterize_mixin_parent_selector file, 'input-size'
84
+ file = replace_ms_filters(file)
85
+ file = replace_all file, /\.\$state/, '.#{$state}'
86
+ file = replace_all file, /,\s*\.open \.dropdown-toggle& \{(.*?)\}/m,
87
+ " {\\1}\n .open & { &.dropdown-toggle {\\1} }"
88
+ when 'responsive-utilities.less'
89
+ file = apply_mixin_parent_selector(file, '&\.(visible|hidden)')
90
+ file = apply_mixin_parent_selector(file, '(?<!&)\.(visible|hidden)')
91
+ file = replace_rules(file, ' @media') { |r| unindent(r, 2) }
92
+ when 'variables.less'
93
+ file = insert_default_vars(file)
94
+ file = replace_all file, /(\$icon-font-path:).*(!default)/, '\1 "bootstrap/" \2'
95
+ when 'close.less'
96
+ # extract .close { button& {...} } rule
97
+ file = extract_nested_rule file, 'button&'
98
+ when 'modals.less'
99
+ file = replace_all file, /body&,(.*?)(\{.*?\})/m, "\\1\\2\nbody& \\2"
100
+ file = extract_nested_rule file, 'body&'
101
+ when 'dropdowns.less'
102
+ file = replace_all file, /(\s*)@extend \.pull-right-dropdown-menu;/, "\\1right: 0;\\1left: auto;"
103
+ when 'forms.less'
104
+ file = extract_nested_rule file, 'textarea&'
105
+ file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
106
+ when 'navbar.less'
107
+ file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
108
+ when 'tables.less'
109
+ file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
110
+ when 'list-group.less'
111
+ file = extract_nested_rule file, 'a&'
112
+ when 'glyphicons.less'
113
+ file = replace_rules(file, '@font-face') { |rule|
114
+ rule = replace_all rule, /(\$icon-font-\w+)/, '#{\1}'
115
+ replace_all rule, /url\(/, 'font-url('
116
+ }
117
+ end
118
+
119
+ name = name.sub(/\.less$/, '.scss')
120
+ save_at = @save_at[:scss]
121
+ path = "#{save_at}/#{'_' unless name == 'bootstrap.scss'}#{name}"
122
+ save_file(path, file)
123
+ log_processed File.basename(path)
124
+ end
125
+ end
126
+
127
+ def store_version
128
+ path = 'lib/bootstrap-sass/version.rb'
129
+ content = File.read(path).sub(/BOOTSTRAP_SHA\s*=\s*['"][\w]+['"]/, "BOOTSTRAP_SHA = '#@branch_sha'")
130
+ File.open(path, 'w') { |f| f.write(content) }
131
+ end
132
+
133
+ def process_javascript_assets
134
+ log_status "Processing javascripts..."
135
+ save_at = @save_at[:js]
136
+ read_files('js', bootstrap_js_files).each do |name, file|
137
+ save_file("#{save_at}/#{name}", file)
138
+ end
139
+ log_processed "#{bootstrap_js_files * ' '}"
140
+
141
+ log_status "Updating javascript manifest"
142
+ content = ''
143
+ bootstrap_js_files.each do |name|
144
+ name = name.gsub(/\.js$/, '')
145
+ content << "//= require bootstrap/#{name}\n"
146
+ end
147
+ path = "vendor/assets/javascripts/bootstrap.js"
148
+ save_file(path, content)
149
+ log_processed path
150
+ end
151
+
152
+ private
153
+
154
+ def read_files(path, files)
155
+ full_path = "#{GIT_RAW}/#@repo/#@branch_sha/#{path}"
156
+ if (contents = read_cached_files(path, files))
157
+ log_http_get_files files, full_path, true
158
+ else
159
+ log_http_get_files files, full_path, false
160
+ contents = {}
161
+ files.map do |name|
162
+ Thread.start {
163
+ content = open("#{full_path}/#{name}").read
164
+ Thread.exclusive { contents[name] = content }
165
+ }
166
+ end.each(&:join)
167
+ write_cached_files path, contents
168
+ end
169
+ contents
170
+ end
171
+
172
+ def read_cached_files(path, files)
173
+ full_path = "#@cache_path/#@branch_sha/#{path}"
174
+ contents = {}
175
+ if File.directory?(full_path)
176
+ files.each do |name|
177
+ contents[name] = File.read("#{full_path}/#{name}", mode: 'rb') || ''
178
+ end
179
+ contents
180
+ end
181
+ end
182
+
183
+ def write_cached_files(path, files)
184
+ full_path = "./#@cache_path/#@branch_sha/#{path}"
185
+ FileUtils.mkdir_p full_path
186
+ files.each do |name, content|
187
+ File.open("#{full_path}/#{name}", 'wb') { |f| f.write content}
188
+ end
189
+ end
190
+
191
+
192
+ def get_file(url)
193
+ cache_path = "./#@cache_path#{URI(url).path}"
194
+ FileUtils.mkdir_p File.dirname(cache_path)
195
+ if File.exists?(cache_path)
196
+ log_http_get_file url, true
197
+ File.read(cache_path, mode: 'rb')
198
+ else
199
+ log_http_get_file url, false
200
+ content = open(url).read
201
+ File.open(cache_path, 'wb') { |f| f.write content }
202
+ content
203
+ end
204
+ end
205
+
206
+ # get sha of the branch (= the latest commit)
207
+ def get_branch_sha
208
+ cmd = "git ls-remote '#@repo_url' | awk '/#@branch/ {print $1}'"
209
+ puts cmd
210
+ @branch_sha ||= %x[#{cmd}].chomp
211
+ raise 'Could not get branch sha!' unless $?.success?
212
+ @branch_sha
213
+ end
214
+
215
+ # Get the sha of a dir
216
+ def get_tree_sha(dir)
217
+ get_trees['tree'].find { |t| t['path'] == dir }['sha']
218
+ end
219
+
220
+ def get_trees
221
+ @trees ||= get_json("#{GIT_DATA}/#@repo/git/trees/#@branch_sha")
222
+ end
223
+
224
+ def bootstrap_font_files
225
+ @bootstrap_font_files ||= begin
226
+ files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('fonts')}"
227
+ files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.(eot|svg|ttf|woff)$/ }.map { |f| f['path'] }
228
+ end
229
+ end
230
+
231
+ def bootstrap_less_files
232
+ @bootstrap_less_files ||= begin
233
+ files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('less')}"
234
+ files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.less$/ }.map { |f| f['path'] }
235
+ end
236
+ end
237
+
238
+ def bootstrap_js_files
239
+ @bootstrap_js_files ||= begin
240
+ files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('js')}"
241
+ files = files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.js$/ }.map { |f| f['path'] }
242
+ files.sort_by { |f|
243
+ case f
244
+ # tooltip depends on popover and must be loaded earlier
245
+ when /tooltip/ then 1
246
+ when /popover/ then 2
247
+ else
248
+ 0
249
+ end
250
+ }
251
+ end
252
+ end
253
+
254
+ # We need to keep a list of shared mixin names in order to convert the includes correctly
255
+ # Before doing any processing we read shared mixins from a file
256
+ # If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
257
+ def read_shared_mixins!(mixins_file)
258
+ log_status " Reading shared mixins from mixins.less"
259
+ @shared_mixins = get_mixin_names(mixins_file, silent: true)
260
+ NESTED_MIXINS.each do |selector, prefix|
261
+ # we use replace_rules without replacing anything just to use the parsing algorithm
262
+ replace_rules(mixins_file, selector) { |rule|
263
+ @shared_mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
264
+ rule
265
+ }
266
+ end
267
+ @shared_mixins.sort!
268
+ log_file_info "shared mixins: #{@shared_mixins * ', '}"
269
+ @shared_mixins
270
+ end
271
+
272
+ def get_mixin_names(file, opts = {})
273
+ names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)[ ]*\{/).map(&:first).uniq.sort
274
+ log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
275
+ names
276
+ end
277
+
278
+ def convert_to_scss(file)
279
+ # mixins may also be defined in the file. get mixin names before doing any processing
280
+ mixin_names = (@shared_mixins + get_mixin_names(file)).uniq
281
+ file = replace_vars(file)
282
+ file = replace_file_imports(file)
283
+ file = replace_mixin_definitions file
284
+ file = replace_mixins file, mixin_names
285
+ # replace_less_extend does not seem to do anything. @glebm
286
+ file = replace_less_extend(file)
287
+ file = replace_spin(file)
288
+ file = replace_image_urls(file)
289
+ file = replace_image_paths(file)
290
+ file = replace_escaping(file)
291
+ file = convert_less_ampersand(file)
292
+ file = deinterpolate_vararg_mixins(file)
293
+ file = replace_calculation_semantics(file)
294
+ file
295
+ end
296
+
297
+ # margin: a -b
298
+ # LESS: sets 2 values
299
+ # SASS: sets 1 value (a-b)
300
+ # This wraps a and -b so they evaluates to 2 values in SASS
301
+ def replace_calculation_semantics(file)
302
+ # split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
303
+ # #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
304
+ split_prop_val = proc { |val|
305
+ s = CharStringScanner.new(val)
306
+ r = []
307
+ buff = ''
308
+ d = 0
309
+ prop_char = %r([\$\w\-/\*\+%!])
310
+ while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
311
+ buff << token
312
+ case token
313
+ when '('
314
+ d += 1
315
+ when ')'
316
+ d -= 1
317
+ if d == 0
318
+ r << buff
319
+ buff = ''
320
+ end
321
+ when /\s/
322
+ if d == 0 && !buff.strip.empty?
323
+ r << buff
324
+ buff = ''
325
+ end
326
+ end
327
+ end
328
+ r << buff unless buff.empty?
329
+ r.map(&:strip)
330
+ }
331
+
332
+ replace_rules file do |rule|
333
+ replace_properties rule do |props|
334
+ props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
335
+ prop, vals = $1, split_prop_val.call($2)
336
+ next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
337
+ transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
338
+ log_transform "property #{prop}: #{transformed * ' '}"
339
+ "#{prop}: #{transformed * ' '};"
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ def save_file(path, content, mode='w')
346
+ File.open(path, mode) { |file| file.write(content) }
347
+ end
348
+
349
+ # @import "file.less" to "#{target_path}file;"
350
+ def replace_file_imports(less, target_path = 'bootstrap/')
351
+ less.gsub %r([@\$]import ["|']([\w-]+).less["|'];),
352
+ %Q(@import "#{target_path}\\1";)
353
+ end
354
+
355
+ def replace_all(file, regex, replacement = nil, &block)
356
+ log_transform regex, replacement
357
+ new_file = file.gsub(regex, replacement, &block)
358
+ raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
359
+ new_file
360
+ end
361
+
362
+ # @mixin a() { tr& { color:white } }
363
+ # to:
364
+ # @mixin a($parent) { tr#{$parent} { color: white } }
365
+ def parameterize_mixin_parent_selector(file, rule_sel)
366
+ log_transform rule_sel
367
+ param = '$parent'
368
+ replace_rules(file, '^[ \t]*@mixin\s*' + rule_sel) do |mxn_css|
369
+ mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
370
+ # insert param into mixin def
371
+ mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
372
+ # wrap properties in #{$parent} { ... }
373
+ replace_properties(mxn_css) { |props| " \#{#{param}} { #{props.strip} }\n " }
374
+ # change nested& rules to nested#{$parent}
375
+ replace_rules(mxn_css, /.*[^\s ]&/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
376
+ end
377
+ end
378
+
379
+ # extracts rule immediately after it's parent, and adjust the selector
380
+ # .x { textarea& { ... }}
381
+ # to:
382
+ # .x { ... }
383
+ # textarea.x { ... }
384
+ def extract_nested_rule(file, selector, new_selector = nil)
385
+ matches = []
386
+ # first find the rules, and remove them
387
+ file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
388
+ matches << [rule, pos]
389
+ new_selector ||= "#{get_selector(rule).sub(/&$/, '')}#{selector_for_pos(css, pos.begin)}"
390
+ indent "// [converter] extracted #{get_selector(rule)} to #{new_selector}", indent_width(rule)
391
+ }
392
+ log_transform selector, new_selector
393
+ # replace rule selector with new_selector
394
+ matches.each do |m|
395
+ m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{new_selector}\\3{"
396
+ end
397
+ replace_substrings_at file,
398
+ matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
399
+ matches.map { |rule, _| "\n\n" + unindent(rule) }
400
+ end
401
+
402
+ # .visible-sm { @include responsive-visibility() }
403
+ # to:
404
+ # @include responsive-visibility('.visible-sm')
405
+ def apply_mixin_parent_selector(file, rule_sel)
406
+ log_transform rule_sel
407
+ replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
408
+ body = unwrap_rule_block(rule.dup).strip
409
+ next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
410
+ rule =~ /(#{COMMENT_RE}*)(#{SELECTOR_RE})\{/
411
+ cmt, sel = $1, $2.strip
412
+ # take one up selector chain if this is an &. selector
413
+ if sel.start_with?('&')
414
+ parent_sel = selector_for_pos(css, rule_pos.begin)
415
+ sel = parent_sel + sel[1..-1]
416
+ end
417
+ # unwrap, and replace @include
418
+ unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(([\$\w\-,\s]*)\)/) {
419
+ "#{cmt}#{$1}('#{sel}'#{', ' if $2 && !$2.empty?}#{$2})"
420
+ }
421
+ end
422
+ end
423
+
424
+ # #gradient > { @mixin horizontal ... }
425
+ # to:
426
+ # @mixin gradient-horizontal
427
+ def flatten_mixins(file, container, prefix)
428
+ log_transform container, prefix
429
+ replace_rules file, Regexp.escape(container) do |mixins_css|
430
+ unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
431
+ end
432
+ end
433
+
434
+ # Replaces the following:
435
+ # .mixin() -> @include mixin()
436
+ # #scope > .mixin() -> @include scope-mixin()
437
+ def replace_mixins(less, mixin_names)
438
+ mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))(?!\s\{)/
439
+
440
+ less.gsub(mixin_pattern) do |match|
441
+ matches = match.scan(mixin_pattern).flatten
442
+ scope = matches[1] || ''
443
+ if scope != ''
444
+ scope = scope.scan(/[\w-]+/).join('-') + '-'
445
+ end
446
+ mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?\{?/).first
447
+ if mixin_name && mixin_names.include?("#{scope}#{mixin_name.first}")
448
+ "#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/, ", $").sub(/;\)$/, ')')
449
+ else
450
+ "#{matches.first}@extend .#{scope}#{matches.last.gsub(/\(\)/, '')}"
451
+ end
452
+ end
453
+ end
454
+
455
+ # change Microsoft filters to SASS calling convention
456
+ def replace_ms_filters(file)
457
+ log_transform
458
+ file.gsub(
459
+ /filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
460
+ %Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
461
+ )
462
+ end
463
+
464
+ # unwraps topmost rule block
465
+ # #sel { a: b; }
466
+ # to:
467
+ # a: b;
468
+ def unwrap_rule_block(css)
469
+ css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
470
+ end
471
+
472
+ def replace_mixin_definitions(less)
473
+ less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
474
+ "#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
475
+ }
476
+ end
477
+
478
+ def replace_vars(less)
479
+ less = less.dup
480
+ # skip header comment
481
+ less =~ %r(\A/\*(.*?)\*/)m
482
+ from = $~ ? $~.to_s.length : 0
483
+ less[from..-1] = less[from..-1].
484
+ gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
485
+ # variables that would be ignored by gsub above: e.g. @page-header-border-color
486
+ gsub(/@(page[\w-]+)/, '$\1')
487
+ less
488
+ end
489
+
490
+ # #gradient > .horizontal()
491
+ # to:
492
+ # @include .horizontal-gradient()
493
+ def replace_less_extend(less)
494
+ less.gsub(/\#(\w+) \> \.([\w-]*)(\(.*\));?/, '@include \1-\2\3;')
495
+ end
496
+
497
+ def replace_spin(less)
498
+ less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
499
+ end
500
+
501
+ def replace_image_urls(less)
502
+ less.gsub(/background-image: url\("?(.*?)"?\);/) {|s| "background-image: image-url(\"#{$1}\");" }
503
+ end
504
+
505
+ def replace_image_paths(less)
506
+ less.gsub('../img/', '')
507
+ end
508
+
509
+ def replace_escaping(less)
510
+ less = less.gsub(/\~"([^"]+)"/, '#{\1}') # Get rid of ~"" escape
511
+ less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
512
+ less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
513
+ less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
514
+ end
515
+
516
+ def insert_default_vars(scss)
517
+ log_transform
518
+ scss.gsub(/^(\$.+);/, '\1 !default;')
519
+ end
520
+
521
+ # Converts &-
522
+ def convert_less_ampersand(less)
523
+ regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
524
+
525
+ tmp = ''
526
+ less.scan(/^(\s*&)(-[\w\[\]]+\s*{.+})$/) do |ampersand, css|
527
+ tmp << ".badge#{css}\n"
528
+ end
529
+
530
+ less.gsub(regx, tmp)
531
+ end
532
+
533
+ # unindent by n spaces
534
+ def unindent(txt, n = 2)
535
+ txt.gsub /^[ ]{#{n}}/, ''
536
+ end
537
+
538
+ # indent by n spaces
539
+ def indent(txt, n = 2)
540
+ "#{' ' * n}#{txt}"
541
+ end
542
+
543
+ # get indent length from the first line of txt
544
+ def indent_width(txt)
545
+ txt.match(/\A\s*/).to_s.length
546
+ end
547
+
548
+ # @mixin transition($transition) {
549
+ # to:
550
+ # @mixin transition($transition...) {
551
+ def varargify_mixin_definitions(scss, *mixins)
552
+ log_transform *mixins
553
+ scss = scss.dup
554
+ mixins.each do |mixin|
555
+ scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
556
+ end
557
+ scss
558
+ end
559
+
560
+ # @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
561
+ # to
562
+ # @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
563
+ def deinterpolate_vararg_mixins(scss)
564
+ scss = scss.dup
565
+ VARARG_MIXINS.each do |mixin|
566
+ if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
567
+ log_transform mixin
568
+ end
569
+ end
570
+ scss
571
+ end
572
+
573
+ # get full selector for rule_block
574
+ def get_selector(rule_block)
575
+ /^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
576
+ end
577
+
578
+ # replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
579
+ # will also include immediately preceding comments in rule_block
580
+ #
581
+ # option :comments -- include immediately preceding comments in rule_block
582
+ #
583
+ # replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
584
+ def replace_rules(less, rule_prefix = SELECTOR_RE, options = {}, &block)
585
+ options = {comments: true}.merge(options || {})
586
+ less = less.dup
587
+ s = CharStringScanner.new(less)
588
+ rule_re = /(?:#{rule_prefix}[^{]*#{RULE_OPEN_BRACE_RE})/
589
+ if options[:comments]
590
+ rule_start_re = /(?:#{COMMENT_RE}*)^#{rule_re}/
591
+ else
592
+ rule_start_re = /^#{rule_re}/
593
+ end
594
+
595
+ positions = []
596
+ while (rule_start = s.scan_next(rule_start_re))
597
+ pos = s.pos
598
+ positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
599
+ end
600
+ replace_substrings_at(less, positions, &block)
601
+ less
602
+ end
603
+
604
+ # Get a all top-level selectors (with {)
605
+ def get_css_selectors(css, opts = {})
606
+ s = CharStringScanner.new(css)
607
+ selectors = []
608
+ while s.scan_next(RULE_OPEN_BRACE_RE)
609
+ brace_pos = s.pos
610
+ def_pos = css_def_pos(css, brace_pos+1, -1)
611
+ sel = css[def_pos.begin..brace_pos - 1].dup
612
+ sel.strip! if opts[:strip]
613
+ selectors << sel
614
+ sel.dup.strip
615
+ s.pos = close_brace_pos(css, brace_pos, 1) + 1
616
+ end
617
+ selectors
618
+ end
619
+
620
+ # replace in the top-level selector
621
+ # replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
622
+ def replace_in_selector(css, pattern, sub)
623
+ # scan for selector positions in css
624
+ s = CharStringScanner.new(css)
625
+ prev_pos = 0
626
+ sel_pos = []
627
+ while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
628
+ pos = s.pos
629
+ sel_pos << (prev_pos .. pos - 1)
630
+ s.pos = close_brace_pos(css, s.pos - 1) + 1
631
+ prev_pos = pos
632
+ end
633
+ replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
634
+ end
635
+
636
+
637
+ sel_chars = '\[\]$\w\-{}#,.:&>@'
638
+ SELECTOR_RE = /[#{sel_chars}]+[#{sel_chars}\s]*/
639
+ COMMENT_RE = %r((?:^[ \t]*//[^\n]*\n))
640
+ RULE_OPEN_BRACE_RE = /(?<![@#\$])\{/
641
+ RULE_OPEN_BRACE_RE_REVERSE = /\{(?![@#\$])/
642
+ RULE_CLOSE_BRACE_RE = /(?<!\w)\}(?![.'"])/
643
+ RULE_CLOSE_BRACE_RE_REVERSE = /(?<![.'"])\}(?!\w)/
644
+ BRACE_RE = /#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m
645
+ BRACE_RE_REVERSE = /#{RULE_OPEN_BRACE_RE_REVERSE}|#{RULE_CLOSE_BRACE_RE_REVERSE}/m
646
+ SCSS_MIXIN_DEF_ARGS_RE = /[\w\-,\s$:#%]*/
647
+ LESS_MIXIN_DEF_ARGS_RE = /[\w\-,;\s@:#%]*/
648
+
649
+ # replace first level properties in the css with yields
650
+ # replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
651
+ def replace_properties(css, &block)
652
+ s = CharStringScanner.new(css)
653
+ s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
654
+ prev_pos = s.pos
655
+ depth = 0
656
+ pos = []
657
+ while (b = s.scan_next(/#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m))
658
+ s_pos = s.pos
659
+ depth += (b == '}' ? -1 : +1)
660
+ if depth == 1
661
+ if b == '}'
662
+ prev_pos = s_pos
663
+ else
664
+ pos << (prev_pos .. s_pos - b.length - 1)
665
+ end
666
+ end
667
+ end
668
+ replace_substrings_at css, pos, &block
669
+ end
670
+
671
+
672
+ # immediate selector of css at pos
673
+ def selector_for_pos(css, pos, depth = -1)
674
+ css[css_def_pos(css, pos, depth)].dup.strip
675
+ end
676
+
677
+ # get the pos of css def at pos (search backwards)
678
+ def css_def_pos(css, pos, depth = -1)
679
+ to = open_brace_pos(css, pos, depth)
680
+ prev_def = to - (css[0..to].reverse.index('}') || to) + 1
681
+ from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
682
+ (from..to - 1)
683
+ end
684
+
685
+ # next matching brace for brace at from
686
+ def close_brace_pos(css, from, depth = 0)
687
+ s = CharStringScanner.new(css[from..-1])
688
+ while (b = s.scan_next(BRACE_RE))
689
+ depth += (b == '}' ? -1 : +1)
690
+ break if depth.zero?
691
+ end
692
+ raise "match not found for {" unless depth.zero?
693
+ from + s.pos - 1
694
+ end
695
+
696
+ # opening brace position from +from+ (search backwards)
697
+ def open_brace_pos(css, from, depth = 0)
698
+ s = CharStringScanner.new(css[0..from].reverse)
699
+ while (b = s.scan_next(BRACE_RE_REVERSE))
700
+ depth += (b == '{' ? +1 : -1)
701
+ break if depth.zero?
702
+ end
703
+ raise "matching { brace not found" unless depth.zero?
704
+ from - s.pos + 1
705
+ end
706
+
707
+ # insert substitutions into text at positions (Range or Fixnum)
708
+ # substitutions can be passed as array or as yields from the &block called with |substring, position, text|
709
+ # position is a range (begin..end)
710
+ def replace_substrings_at(text, positions, replacements = nil, &block)
711
+ offset = 0
712
+ positions.each_with_index do |p, i|
713
+ p = (p...p) if p.is_a?(Fixnum)
714
+ from = p.begin + offset
715
+ to = p.end + offset
716
+ p = p.exclude_end? ? (from...to) : (from..to)
717
+ # block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
718
+ r = replacements ? replacements[i] : block.call(text[p], p, text)
719
+ text[p] = r
720
+ # add the change in length to offset
721
+ offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
722
+ end
723
+ text
724
+ end
725
+
726
+ def get_json(url)
727
+ JSON.parse get_file(url)
728
+ end
729
+
730
+ # regular string scanner works with bytes
731
+ # this one works with chars and provides #scan_next
732
+ class CharStringScanner
733
+ extend Forwardable
734
+
735
+ def initialize(*args)
736
+ @s = StringScanner.new(*args)
737
+ end
738
+
739
+ def_delegators :@s, :scan_until, :skip_until, :string
740
+
741
+ # advance scanner to pos after the next match of pattern and return the match
742
+ def scan_next(pattern)
743
+ return unless @s.scan_until(pattern)
744
+ @s.matched
745
+ end
746
+
747
+ def pos
748
+ byte_to_str_pos @s.pos
749
+ end
750
+
751
+ def pos=(i)
752
+ @s.pos = str_to_byte_pos i
753
+ i
754
+ end
755
+
756
+ private
757
+
758
+ def byte_to_str_pos(pos)
759
+ @s.string.byteslice(0, pos).length
760
+ end
761
+
762
+ def str_to_byte_pos(pos)
763
+ @s.string.slice(0, pos).bytesize
764
+ end
765
+ end
766
+
767
+ class Logger
768
+ include Term::ANSIColor
769
+
770
+ def initialize(env)
771
+ @env = env
772
+ puts bold "Convert Bootstrap LESS to SASS"
773
+ puts " repo : #{env[:repo]}"
774
+ puts " branch : #{env[:branch]} #{dark "#{env[:repo]}/tree/#{env[:branch_sha]}"}"
775
+ puts " save to: #{@env[:save_at].to_json}"
776
+ puts " twbs cache: #{@env[:cache_path]}"
777
+ puts dark "-" * 60
778
+ end
779
+
780
+ def log_status(status)
781
+ puts bold status
782
+ end
783
+
784
+ def log_file_info(s)
785
+ puts " #{magenta s}"
786
+ end
787
+
788
+ def log_transform(*args)
789
+ puts "#{cyan " #{caller[1][/`.*'/][1..-2].sub(/^block in /, '')}"}#{cyan ": #{args * ', '}" unless args.empty?}"
790
+ end
791
+
792
+ def log_processing(name)
793
+ puts yellow " #{File.basename(name)}"
794
+ end
795
+
796
+ def log_processed(name)
797
+ puts green " #{name}"
798
+ end
799
+
800
+ def log_http_get_file(url, cached = false)
801
+ s = " #{'CACHED ' if cached}GET #{url}..."
802
+ if cached
803
+ puts dark green s
804
+ else
805
+ puts dark cyan s
806
+ end
807
+ end
808
+
809
+ def log_http_get_files(files, from, cached = false)
810
+ s = " #{'CACHED ' if cached}GET #{files.length} files from #{from} #{files * ' '}..."
811
+ if cached
812
+ puts dark green s
813
+ else
814
+ puts dark cyan s
815
+ end
816
+ end
817
+
818
+ def puts(*args)
819
+ STDOUT.puts *args unless @silence
820
+ end
821
+
822
+ def silence_log
823
+ @silence = true
824
+ yield
825
+ ensure
826
+ @silence = false
827
+ end
828
+ end
829
+ end