bootstrap-sass 2.3.1.3 → 3.4.1

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