bootstrap-sass 2.3.1.3 → 3.4.1

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