style-sass 1.0.0

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 (120) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.travis.yml +25 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +21 -0
  6. data/README.md +195 -0
  7. data/Rakefile +99 -0
  8. data/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
  9. data/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +288 -0
  10. data/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
  11. data/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
  12. data/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 +0 -0
  13. data/assets/images/.keep +0 -0
  14. data/assets/javascripts/bootstrap-sprockets.js +12 -0
  15. data/assets/javascripts/bootstrap.js +2363 -0
  16. data/assets/javascripts/bootstrap.min.js +7 -0
  17. data/assets/javascripts/bootstrap/affix.js +162 -0
  18. data/assets/javascripts/bootstrap/alert.js +94 -0
  19. data/assets/javascripts/bootstrap/button.js +120 -0
  20. data/assets/javascripts/bootstrap/carousel.js +237 -0
  21. data/assets/javascripts/bootstrap/collapse.js +211 -0
  22. data/assets/javascripts/bootstrap/dropdown.js +165 -0
  23. data/assets/javascripts/bootstrap/modal.js +337 -0
  24. data/assets/javascripts/bootstrap/popover.js +108 -0
  25. data/assets/javascripts/bootstrap/scrollspy.js +172 -0
  26. data/assets/javascripts/bootstrap/tab.js +155 -0
  27. data/assets/javascripts/bootstrap/tooltip.js +514 -0
  28. data/assets/javascripts/bootstrap/transition.js +59 -0
  29. data/assets/stylesheets/_bootstrap-compass.scss +9 -0
  30. data/assets/stylesheets/_bootstrap-mincer.scss +19 -0
  31. data/assets/stylesheets/_bootstrap-sprockets.scss +9 -0
  32. data/assets/stylesheets/_bootstrap.scss +56 -0
  33. data/assets/stylesheets/_font-awesome.css.scss +2026 -0
  34. data/assets/stylesheets/_normalize.css.scss +427 -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 +270 -0
  41. data/assets/stylesheets/bootstrap/_close.scss +36 -0
  42. data/assets/stylesheets/bootstrap/_code.scss +69 -0
  43. data/assets/stylesheets/bootstrap/_component-animations.scss +37 -0
  44. data/assets/stylesheets/bootstrap/_dropdowns.scss +216 -0
  45. data/assets/stylesheets/bootstrap/_forms.scss +617 -0
  46. data/assets/stylesheets/bootstrap/_glyphicons.scss +307 -0
  47. data/assets/stylesheets/bootstrap/_grid.scss +84 -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 +130 -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 +662 -0
  56. data/assets/stylesheets/bootstrap/_navs.scss +242 -0
  57. data/assets/stylesheets/bootstrap/_normalize.scss +424 -0
  58. data/assets/stylesheets/bootstrap/_pager.scss +54 -0
  59. data/assets/stylesheets/bootstrap/_pagination.scss +89 -0
  60. data/assets/stylesheets/bootstrap/_panels.scss +271 -0
  61. data/assets/stylesheets/bootstrap/_popovers.scss +131 -0
  62. data/assets/stylesheets/bootstrap/_print.scss +101 -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 +291 -0
  69. data/assets/stylesheets/bootstrap/_thumbnails.scss +38 -0
  70. data/assets/stylesheets/bootstrap/_tooltip.scss +101 -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 +14 -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 +65 -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 +58 -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 +33 -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 +8 -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 +21 -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/lib/styler-sass.rb +94 -0
  106. data/lib/styler-sass/engine.rb +17 -0
  107. data/lib/styler-sass/version.rb +3 -0
  108. data/styler-sass.gemspec +48 -0
  109. data/tasks/bower.rake +31 -0
  110. data/tasks/converter.rb +80 -0
  111. data/tasks/converter/char_string_scanner.rb +38 -0
  112. data/tasks/converter/fonts_conversion.rb +16 -0
  113. data/tasks/converter/js_conversion.rb +47 -0
  114. data/tasks/converter/less_conversion.rb +714 -0
  115. data/tasks/converter/logger.rb +57 -0
  116. data/tasks/converter/network.rb +97 -0
  117. data/templates/project/_bootstrap-variables.sass +875 -0
  118. data/templates/project/manifest.rb +20 -0
  119. data/templates/project/styles.sass +6 -0
  120. metadata +449 -0
@@ -0,0 +1,94 @@
1
+ require 'styler-sass/version'
2
+ module Bootstrap
3
+ class << self
4
+ # Inspired by Kaminari
5
+ def load!
6
+ register_compass_extension if compass?
7
+
8
+ if rails?
9
+ register_rails_engine
10
+ elsif lotus?
11
+ register_lotus
12
+ elsif sprockets?
13
+ register_sprockets
14
+ end
15
+
16
+ configure_sass
17
+ end
18
+
19
+ # Paths
20
+ def gem_path
21
+ @gem_path ||= File.expand_path '..', File.dirname(__FILE__)
22
+ end
23
+
24
+ def stylesheets_path
25
+ File.join assets_path, 'stylesheets'
26
+ end
27
+
28
+ def fonts_path
29
+ File.join assets_path, 'fonts'
30
+ end
31
+
32
+ def javascripts_path
33
+ File.join assets_path, 'javascripts'
34
+ end
35
+
36
+ def assets_path
37
+ @assets_path ||= File.join gem_path, 'assets'
38
+ end
39
+
40
+ # Environment detection helpers
41
+ def sprockets?
42
+ defined?(::Sprockets)
43
+ end
44
+
45
+ def compass?
46
+ defined?(::Compass::Frameworks)
47
+ end
48
+
49
+ def rails?
50
+ defined?(::Rails)
51
+ end
52
+
53
+ def lotus?
54
+ defined?(::Lotus)
55
+ end
56
+
57
+ private
58
+
59
+ def configure_sass
60
+ require 'sass'
61
+
62
+ ::Sass.load_paths << stylesheets_path
63
+
64
+ # bootstrap requires minimum precision of 8, see https://github.com/twbs/bootstrap-sass/issues/409
65
+ ::Sass::Script::Number.precision = [8, ::Sass::Script::Number.precision].max
66
+ end
67
+
68
+ def register_compass_extension
69
+ ::Compass::Frameworks.register(
70
+ 'bootstrap',
71
+ :version => Bootstrap::VERSION,
72
+ :path => gem_path,
73
+ :stylesheets_directory => stylesheets_path,
74
+ :templates_directory => File.join(gem_path, 'templates')
75
+ )
76
+ end
77
+
78
+ def register_rails_engine
79
+ require 'styler-sass/engine'
80
+ end
81
+
82
+ def register_lotus
83
+ Lotus::Assets.sources << assets_path
84
+ end
85
+
86
+ def register_sprockets
87
+ Sprockets.append_path(stylesheets_path)
88
+ Sprockets.append_path(fonts_path)
89
+ Sprockets.append_path(javascripts_path)
90
+ end
91
+ end
92
+ end
93
+
94
+ Bootstrap.load!
@@ -0,0 +1,17 @@
1
+ module Styler
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ initializer 'styler-sass.assets.precompile' do |app|
5
+ %w(stylesheets javascripts fonts images).each do |sub|
6
+ app.config.assets.paths << root.join('assets', sub).to_s
7
+ end
8
+
9
+ # sprockets-rails 3 tracks down the calls to `font_path` and `image_path`
10
+ # and automatically precompiles the referenced assets.
11
+ unless Sprockets::Rails::VERSION.split('.', 2)[0].to_i >= 3
12
+ app.config.assets.precompile << %r(bootstrap/glyphicons-halflings-regular\.(?:eot|svg|ttf|woff2?)$)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Styler
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,48 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'styler-sass/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'style-sass'
7
+ spec.version = Styler::VERSION
8
+ spec.authors = ['Alinuxfriend']
9
+ spec.email = ["ajoshi856@gmail.com"]
10
+ spec.summary = ''
11
+ spec.homepage = 'https://github.com/Alinuxfriend/styler-sass'
12
+ spec.license = 'MIT'
13
+
14
+ if spec.respond_to?(:metadata)
15
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
16
+ else
17
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
18
+ end
19
+
20
+
21
+ spec.files = `git ls-files`.split("\n")
22
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ spec.require_paths = ["lib"]
24
+
25
+
26
+ spec.add_runtime_dependency 'sass', '~> 3.3.4'
27
+ spec.add_runtime_dependency 'autoprefixer-rails', '~> 5.2.1'
28
+
29
+ # Testing dependencies
30
+ spec.add_development_dependency 'minitest', '~> 5.8'
31
+ spec.add_development_dependency 'minitest-reporters', '~> 1.1'
32
+ # Integration testing
33
+ spec.add_development_dependency 'capybara', '~> 2.5.0'
34
+ spec.add_development_dependency 'poltergeist'
35
+ # Dummy Rails app dependencies
36
+ spec.add_development_dependency 'actionpack', '~> 4.1.5'
37
+ spec.add_development_dependency 'activesupport', '~> 4.1.5'
38
+ spec.add_development_dependency 'json', '~> 1.8.1'
39
+ spec.add_development_dependency 'sprockets-rails', '~> 2.1.3'
40
+ spec.add_development_dependency 'jquery-rails', '~> 3.1.0'
41
+ spec.add_development_dependency 'slim-rails'
42
+ spec.add_development_dependency 'uglifier'
43
+ # Converter
44
+ spec.add_development_dependency 'term-ansicolor'
45
+
46
+ spec.files = `git ls-files`.split("\n")
47
+ spec.test_files = `git ls-files -- test/*`.split("\n")
48
+ end
@@ -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,714 @@
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
+ if name.start_with?('mixins/')
71
+ file = varargify_mixin_definitions(file, *VARARG_MIXINS)
72
+ %w(responsive-(in)?visibility input-size text-emphasis-variant bg-variant).each do |mixin|
73
+ file = parameterize_mixin_parent_selector file, mixin if file =~ /#{mixin}/
74
+ end
75
+ NESTED_MIXINS.each do |sel, name|
76
+ file = flatten_mixins(file, sel, name) if /#{Regexp.escape(sel)}/ =~ file
77
+ end
78
+ file = replace_all file, /(?<=[.-])\$state/, '#{$state}' if file =~ /[.-]\$state/
79
+ end
80
+ case name
81
+ when 'mixins/buttons.less'
82
+ file = replace_all file, /(\.dropdown-toggle)&/, '&\1'
83
+ when 'mixins/list-group.less'
84
+ file = replace_rules(file, ' .list-group-item-') { |rule| extract_nested_rule rule, 'a&' }
85
+ when 'mixins/gradients.less'
86
+ file = replace_ms_filters(file)
87
+ file = deinterpolate_vararg_mixins(file)
88
+ when 'mixins/vendor-prefixes.less'
89
+ # remove second scale mixins as this is handled via vararg in the first one
90
+ file = replace_rules(file, Regexp.escape('@mixin scale($ratioX, $ratioY...)')) { '' }
91
+ when 'mixins/grid-framework.less'
92
+ file = convert_grid_mixins file
93
+ when 'component-animations.less'
94
+ file = extract_nested_rule file, "#{SELECTOR_RE}&\\.in"
95
+ when 'responsive-utilities.less'
96
+ file = apply_mixin_parent_selector file, '\.(?:visible|hidden)'
97
+ when 'variables.less'
98
+ file = insert_default_vars(file)
99
+ file = ['$bootstrap-sass-asset-helper: false !default;', file].join("\n")
100
+ file = replace_all file, %r{(\$icon-font-path): \s*"(.*)" (!default);}, "\n" + unindent(<<-SCSS, 14)
101
+ // [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
102
+ // [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
103
+ \\1: if($bootstrap-sass-asset-helper, "bootstrap/", "\\2bootstrap/") \\3;
104
+ SCSS
105
+ when 'breadcrumbs.less'
106
+ file = replace_all file, /(.*)(\\00a0)/, unindent(<<-SCSS, 8) + "\\1\#{$nbsp}"
107
+ // [converter] Workaround for https://github.com/sass/libsass/issues/1115
108
+ $nbsp: "\\2";
109
+ SCSS
110
+ when 'close.less'
111
+ # extract .close { button& {...} } rule
112
+ file = extract_nested_rule file, 'button&'
113
+ when 'dropdowns.less'
114
+ file = replace_all file, /@extend \.dropdown-menu-right;/, 'right: 0; left: auto;'
115
+ file = replace_all file, /@extend \.dropdown-menu-left;/, 'left: 0; right: auto;'
116
+ when 'forms.less'
117
+ file = extract_nested_rule file, 'textarea&'
118
+ file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
119
+ file = replace_rules file, /\.form-group-(?:sm|lg)/ do |rule|
120
+ apply_mixin_parent_selector rule, '.form-control'
121
+ end
122
+ when 'navbar.less'
123
+ file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
124
+ when 'tables.less'
125
+ file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
126
+ when 'thumbnails.less', 'labels.less', 'badges.less', 'buttons.less'
127
+ file = extract_nested_rule file, 'a&'
128
+ when 'glyphicons.less'
129
+ file = replace_rules(file, /\s*@font-face/) { |rule|
130
+ rule = replace_all rule, /(\$icon-font(?:-\w+)+)/, '#{\1}'
131
+ replace_asset_url rule, :font
132
+ }
133
+ when 'type.less'
134
+ file = apply_mixin_parent_selector(file, '\.(text|bg)-(success|primary|info|warning|danger)')
135
+ # .bg-primary will not get patched automatically as it includes an additional rule. fudge for now
136
+ file = replace_all(file, " @include bg-variant($brand-primary);\n}", "}\n@include bg-variant('.bg-primary', $brand-primary);")
137
+ end
138
+
139
+ path = File.join save_to, name.sub(/\.less$/, '.scss')
140
+ path = File.join File.dirname(path), '_' + File.basename(path)
141
+ save_file(path, file)
142
+ log_processed File.basename(path)
143
+ end
144
+
145
+ # move bootstrap/_bootstrap.scss to _bootstrap.scss adjusting import paths
146
+ main_from = "#{save_to}/_bootstrap.scss"
147
+ main_to = File.expand_path("#{save_to}/../_bootstrap.scss")
148
+ save_file main_to, File.read(main_from).gsub(/ "/, ' "bootstrap/')
149
+ File.delete(main_from)
150
+
151
+ # generate variables template
152
+ save_file 'templates/project/_bootstrap-variables.sass',
153
+ "// Override Bootstrap variables here (defaults from bootstrap-sass v#{Bootstrap::VERSION}):\n\n" +
154
+ File.read("#{save_to}/_variables.scss").lines[1..-1].join.gsub(/^(?=\$)/, '// ').gsub(/ !default;/, '')
155
+ end
156
+
157
+ def bootstrap_less_files
158
+ @bootstrap_less_files ||= get_paths_by_type('less', /\.less$/)
159
+ end
160
+
161
+ # apply general less to scss conversion
162
+ def convert_to_scss(file)
163
+ # get local mixin names before converting the definitions
164
+ mixins = shared_mixins + read_mixins(file)
165
+ file = replace_vars(file)
166
+ file = replace_mixin_definitions(file)
167
+ file = replace_mixins(file, mixins)
168
+ file = extract_mixins_from_selectors(file, CLASSES_TO_MIXINS.inject({}) { |h, cl| h.update(".#{cl}" => cl) })
169
+ file = replace_spin(file)
170
+ file = replace_fadein(file)
171
+ file = replace_image_urls(file)
172
+ file = replace_escaping(file)
173
+ file = convert_less_ampersand(file)
174
+ file = deinterpolate_vararg_mixins(file)
175
+ file = replace_calculation_semantics(file)
176
+ file = replace_file_imports(file)
177
+ file = wrap_at_groups_with_at_root(file)
178
+ file
179
+ end
180
+
181
+ def wrap_at_groups_with_at_root(file)
182
+ replace_rules(file, /@(?:font-face|-ms-viewport)/) { |rule, _pos|
183
+ %Q(@at-root {\n#{indent rule, 2}\n})
184
+ }
185
+ end
186
+
187
+ def sass_fn_exists(fn)
188
+ %Q{(#{fn}("") != unquote('#{fn}("")'))}
189
+ end
190
+
191
+ def replace_asset_url(rule, type)
192
+ replace_all rule, /url\((.*?)\)/, "url(if($bootstrap-sass-asset-helper, twbs-#{type}-path(\\1), \\1))"
193
+ end
194
+
195
+ # convert recursively evaluated selector $list to @for loop
196
+ def mixin_all_grid_columns(css, selector: raise('pass class'), from: 1, to: raise('pass to'))
197
+ mxn_def = css.each_line.first.strip
198
+ # inject local variables as default arguments
199
+ # this is to avoid overwriting outer variables with the same name with Sass <= 3.3
200
+ # see also: https://github.com/twbs/bootstrap-sass/issues/636
201
+ locals = <<-SASS.strip
202
+ $i: #{from}, $list: "#{selector}"
203
+ SASS
204
+ mxn_def.sub!(/(\(?)(\)\s*\{)/) { "#{$1}#{', ' if $1.empty?}#{locals}#{$2}" }
205
+ step_body = (css =~ /\$list \{\n(.*?)\n[ ]*\}/m) && $1
206
+ <<-SASS
207
+ // [converter] This is defined recursively in LESS, but Sass supports real loops
208
+ #{mxn_def}
209
+ @for $i from (#{from} + 1) through #{to} {
210
+ $list: "\#{$list}, #{selector}";
211
+ }
212
+ \#{$list} {
213
+ #{unindent step_body, 2}
214
+ }
215
+ }
216
+ SASS
217
+ end
218
+
219
+ # convert grid mixins LESS when => Sass @if
220
+ def convert_grid_mixins(file)
221
+ file = replace_rules file, /@mixin make-grid-columns/, comments: false do |css, pos|
222
+ mixin_all_grid_columns css, selector: '.col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}', to: '$grid-columns'
223
+ end
224
+ file = replace_rules file, /@mixin float-grid-columns/, comments: false do |css, pos|
225
+ mixin_all_grid_columns css, selector: '.col-#{$class}-#{$i}', to: '$grid-columns'
226
+ end
227
+ file = replace_rules file, /@mixin calc-grid-column/ do |css|
228
+ css = indent css.gsub(/.*when (.*?) {/, '@if \1 {').gsub(/(\$[\w-]+)\s+=\s+(\w+)/, '\1 == \2').gsub(/(?<=-)(\$[a-z]+)/, '#{\1}')
229
+ if css =~ /== width/
230
+ css = "@mixin calc-grid-column($index, $class, $type) {\n#{css}"
231
+ elsif css =~ /== offset/
232
+ css += "\n}"
233
+ end
234
+ css
235
+ end
236
+ file = replace_rules file, /@mixin loop-grid-columns/ do |css|
237
+ unindent <<-SASS, 8
238
+ // [converter] This is defined recursively in LESS, but Sass supports real loops
239
+ @mixin loop-grid-columns($columns, $class, $type) {
240
+ @for $i from 0 through $columns {
241
+ @include calc-grid-column($i, $class, $type);
242
+ }
243
+ }
244
+ SASS
245
+ end
246
+ file
247
+ end
248
+
249
+
250
+ # We need to keep a list of shared mixin names in order to convert the includes correctly
251
+ # Before doing any processing we read shared mixins from a file
252
+ # If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
253
+ def read_mixins(mixins_file, nested: {})
254
+ mixins = get_mixin_names(mixins_file, silent: true)
255
+ nested.each do |selector, prefix|
256
+ # we use replace_rules without replacing anything just to use the parsing algorithm
257
+ replace_rules(mixins_file, selector) { |rule|
258
+ mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
259
+ rule
260
+ }
261
+ end
262
+ mixins.uniq!
263
+ mixins.sort!
264
+ log_file_info "mixins: #{mixins * ', '}" unless mixins.empty?
265
+ mixins
266
+ end
267
+
268
+ def get_mixin_names(file, opts = {})
269
+ names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)(?: when.*?)?[ ]*\{/).map(&:first).uniq.sort
270
+ log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
271
+ names
272
+ end
273
+
274
+ # margin: a -b
275
+ # LESS: sets 2 values
276
+ # Sass: sets 1 value (a-b)
277
+ # This wraps a and -b so they evaluates to 2 values in Sass
278
+ def replace_calculation_semantics(file)
279
+ # split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
280
+ # #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
281
+ split_prop_val = proc { |val|
282
+ s = CharStringScanner.new(val)
283
+ r = []
284
+ buff = ''
285
+ d = 0
286
+ prop_char = %r([\$\w\-/\*\+%!])
287
+ while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
288
+ buff << token
289
+ case token
290
+ when '('
291
+ d += 1
292
+ when ')'
293
+ d -= 1
294
+ if d == 0
295
+ r << buff
296
+ buff = ''
297
+ end
298
+ when /\s/
299
+ if d == 0 && !buff.strip.empty?
300
+ r << buff
301
+ buff = ''
302
+ end
303
+ end
304
+ end
305
+ r << buff unless buff.empty?
306
+ r.map(&:strip)
307
+ }
308
+
309
+ replace_rules file do |rule|
310
+ replace_properties rule do |props|
311
+ props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
312
+ prop, vals = $1, split_prop_val.call($2)
313
+ next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
314
+ transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
315
+ log_transform "property #{prop}: #{transformed * ' '}", from: 'wrap_calculation'
316
+ "#{prop}: #{transformed * ' '};"
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ # @import "file.less" to "#{target_path}file;"
323
+ def replace_file_imports(less, target_path = '')
324
+ less.gsub %r([@\$]import ["|']([\w\-/]+).less["|'];),
325
+ %Q(@import "#{target_path}\\1";)
326
+ end
327
+
328
+ def replace_all(file, regex, replacement = nil, &block)
329
+ log_transform regex, replacement
330
+ new_file = file.gsub(regex, replacement, &block)
331
+ raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
332
+ new_file
333
+ end
334
+
335
+ # @mixin a() { tr& { color:white } }
336
+ # to:
337
+ # @mixin a($parent) { tr#{$parent} { color: white } }
338
+ def parameterize_mixin_parent_selector(file, rule_sel)
339
+ log_transform rule_sel
340
+ param = '$parent'
341
+ replace_rules(file, '^\s*@mixin\s*' + rule_sel) do |mxn_css|
342
+ mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
343
+ # insert param into mixin def
344
+ mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
345
+ # wrap properties in #{$parent} { ... }
346
+ replace_properties(mxn_css) { |props|
347
+ next props if props.strip.empty?
348
+ spacer = ' ' * indent_width(props)
349
+ "#{spacer}\#{#{param}} {\n#{indent(props.sub(/\s+\z/, ''), 2)}\n#{spacer}}"
350
+ }
351
+ # change nested& rules to nested#{$parent}
352
+ replace_rules(mxn_css, /.*&[ ,:]/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
353
+ end
354
+ end
355
+
356
+ # extracts rule immediately after it's parent, and adjust the selector
357
+ # .x { textarea& { ... }}
358
+ # to:
359
+ # .x { ... }
360
+ # textarea.x { ... }
361
+ def extract_nested_rule(file, selector, new_selector = nil)
362
+ matches = []
363
+ # first find the rules, and remove them
364
+ file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
365
+ new_sel = new_selector || "#{get_selector(rule).gsub(/&/, selector_for_pos(css, pos.begin))}"
366
+ matches << [rule, pos, new_sel]
367
+ indent "// [converter] extracted #{get_selector(rule)} to #{new_sel}".tr("\n", ' ').squeeze(' '), indent_width(rule)
368
+ }
369
+ raise "extract_nested_rule: no such selector: #{selector}" if matches.empty?
370
+ # replace rule selector with new_selector
371
+ matches.each do |m|
372
+ m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{m[2]}\\3{"
373
+ log_transform selector, m[2]
374
+ end
375
+ replace_substrings_at file,
376
+ matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
377
+ matches.map { |rule, _| "\n\n" + unindent(rule) }
378
+ end
379
+
380
+ # .visible-sm { @include responsive-visibility() }
381
+ # to:
382
+ # @include responsive-visibility('.visible-sm')
383
+ def apply_mixin_parent_selector(file, rule_sel)
384
+ log_transform rule_sel
385
+ replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
386
+ body = unwrap_rule_block(rule.dup).strip
387
+ next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
388
+ rule =~ /(#{COMMENT_RE}*)([#{SELECTOR_CHAR}\s*]+?)#{RULE_OPEN_BRACE_RE}/
389
+ cmt, sel = $1, $2.strip
390
+ # take one up selector chain if this is an &. selector
391
+ if sel.start_with?('&')
392
+ parent_sel = selector_for_pos(css, rule_pos.begin)
393
+ sel = parent_sel + sel[1..-1]
394
+ end
395
+ # unwrap, and replace @include
396
+ unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(?([\$\w\-,\s]*)\)?/) {
397
+ name, args = $1, $2
398
+ sel.gsub(/\s+/, ' ').split(/,\s*/ ).map { |sel_part|
399
+ "#{cmt}#{name}('#{sel_part}'#{', ' if args && !args.empty?}#{args})"
400
+ }.join(";\n")
401
+ }
402
+ end
403
+ end
404
+
405
+ # #gradient > { @mixin horizontal ... }
406
+ # to:
407
+ # @mixin gradient-horizontal
408
+ def flatten_mixins(file, container, prefix)
409
+ log_transform container, prefix
410
+ replace_rules file, Regexp.escape(container) do |mixins_css|
411
+ unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
412
+ end
413
+ end
414
+
415
+ # .btn { ... } -> @mixin btn { ... }; .btn { @include btn }
416
+ def extract_mixins_from_selectors(file, selectors_to_mixins)
417
+ selectors_to_mixins.each do |selector, mixin|
418
+ file = replace_rules file, Regexp.escape(selector), prefix: false do |selector_css|
419
+ log_transform "#{selector} { ... } -> @mixin #{mixin} { ... }; #{selector} { @include #{mixin} } ", from: 'extract_mixins_from_selectors'
420
+ <<-SCSS
421
+ // [converter] extracted from `#{selector}` for libsass compatibility
422
+ @mixin #{mixin} {#{unwrap_rule_block(selector_css)}
423
+ }
424
+ // [converter] extracted as `@mixin #{mixin}` for libsass compatibility
425
+ #{selector} {
426
+ @include #{mixin};
427
+ }
428
+ SCSS
429
+ end
430
+ end
431
+ file
432
+ end
433
+
434
+ # @include and @extend from LESS:
435
+ # .mixin() -> @include mixin()
436
+ # #scope > .mixin() -> @include scope-mixin()
437
+ # &:extend(.mixin all) -> @include mixin()
438
+ def replace_mixins(less, mixin_names)
439
+ mixin_pattern = /(?<=^|\s)((?:[#|\.][\w-]+\s*>\s*)*)\.([\w-]+)\((.*)\)(?!\s\{)/
440
+
441
+ less = less.gsub(mixin_pattern) do |_|
442
+ scope, name, args = $1, $2, $3
443
+ scope = scope.scan(/[\w-]+/).join('-') + '-' unless scope.empty?
444
+ args = "(#{args.tr(';', ',')})" unless args.empty?
445
+ if name && mixin_names.include?("#{scope}#{name}")
446
+ "@include #{scope}#{name}#{args}"
447
+ else
448
+ "@extend .#{scope}#{name}"
449
+ end
450
+ end
451
+
452
+ less.gsub /&:extend\((#{SELECTOR_RE})(?: all)?\)/ do
453
+ selector = $1
454
+ selector =~ /\.([\w-]+)/
455
+ mixin = $1
456
+ if mixin && mixin_names.include?(mixin)
457
+ "@include #{mixin}"
458
+ else
459
+ "@extend #{selector}"
460
+ end
461
+ end
462
+ end
463
+
464
+ # change Microsoft filters to Sass calling convention
465
+ def replace_ms_filters(file)
466
+ log_transform
467
+ file.gsub(
468
+ /filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
469
+ %Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
470
+ )
471
+ end
472
+
473
+ # unwraps topmost rule block
474
+ # #sel { a: b; }
475
+ # to:
476
+ # a: b;
477
+ def unwrap_rule_block(css)
478
+ css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
479
+ end
480
+
481
+ def replace_mixin_definitions(less)
482
+ less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
483
+ "#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
484
+ }
485
+ end
486
+
487
+ def replace_vars(less)
488
+ less = less.dup
489
+ # skip header comment
490
+ less =~ %r(\A/\*(.*?)\*/)m
491
+ from = $~ ? $~.to_s.length : 0
492
+ less[from..-1] = less[from..-1].
493
+ gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
494
+ # variables that would be ignored by gsub above: e.g. @page-header-border-color
495
+ gsub(/@(page[\w-]+)/, '$\1')
496
+ less
497
+ end
498
+
499
+ def replace_spin(less)
500
+ less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
501
+ end
502
+
503
+ def replace_fadein(less)
504
+ less.gsub(/(?![\-$@.])fadein\((.*?),\s*(.*?)%\)/) { "fade_in(#{$1}, #{$2.to_i / 100.0})" }
505
+ end
506
+
507
+ def replace_image_urls(less)
508
+ less.gsub(/background-image: url\("?(.*?)"?\);/) { |s| replace_asset_url s, :image }
509
+ end
510
+
511
+ def replace_escaping(less)
512
+ less = less.gsub(/~"([^"]+)"/, '\1').gsub(/~'([^']+)'/, '\1') # Get rid of ~"" escape
513
+ less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
514
+ less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
515
+ less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
516
+ end
517
+
518
+ def insert_default_vars(scss)
519
+ log_transform
520
+ scss.gsub(/^(\$.+);/, '\1 !default;')
521
+ end
522
+
523
+ # Converts &-
524
+ def convert_less_ampersand(less)
525
+ regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
526
+
527
+ tmp = ''
528
+ less.scan(/^(\s*&)(-[\w\[\]]+\s*\{.+})$/) do |ampersand, css|
529
+ tmp << ".badge#{css}\n"
530
+ end
531
+
532
+ less.gsub(regx, tmp)
533
+ end
534
+
535
+ # unindent by n spaces
536
+ def unindent(txt, n = 2)
537
+ txt.gsub /^[ ]{#{n}}/, ''
538
+ end
539
+
540
+ # indent by n spaces
541
+ def indent(txt, n = 2)
542
+ spaces = ' ' * n
543
+ txt.gsub /^/, spaces
544
+ end
545
+
546
+ # get indent length from the first line of txt
547
+ def indent_width(txt)
548
+ txt.match(/\A\s*/).to_s.length
549
+ end
550
+
551
+ # @mixin transition($transition) {
552
+ # to:
553
+ # @mixin transition($transition...) {
554
+ def varargify_mixin_definitions(scss, *mixins)
555
+ scss = scss.dup
556
+ replaced = []
557
+ mixins.each do |mixin|
558
+ if scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
559
+ replaced << mixin
560
+ end
561
+ end
562
+ log_transform *replaced unless replaced.empty?
563
+ scss
564
+ end
565
+
566
+ # @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
567
+ # to
568
+ # @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
569
+ def deinterpolate_vararg_mixins(scss)
570
+ scss = scss.dup
571
+ VARARG_MIXINS.each do |mixin|
572
+ if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
573
+ log_transform mixin
574
+ end
575
+ end
576
+ scss
577
+ end
578
+
579
+ # get full selector for rule_block
580
+ def get_selector(rule_block)
581
+ sel = /^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
582
+ sel.sub /\s*\{\n\s.*/m, ''
583
+ end
584
+
585
+ # replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
586
+ # will also include immediately preceding comments in rule_block
587
+ #
588
+ # option :comments -- include immediately preceding comments in rule_block
589
+ #
590
+ # replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
591
+ def replace_rules(less, selector = SELECTOR_RE, options = {}, &block)
592
+ options = {prefix: true, comments: true}.merge(options || {})
593
+ less = less.dup
594
+ s = CharStringScanner.new(less)
595
+ rule_re = if options[:prefix]
596
+ /(?:#{selector}[#{SELECTOR_CHAR})=(\s]*?#{RULE_OPEN_BRACE_RE})/
597
+ else
598
+ /#{selector}[\s]*#{RULE_OPEN_BRACE_RE}/
599
+ end
600
+ rule_start_re = if options[:comments]
601
+ /(?:#{COMMENT_RE}*)^#{rule_re}/
602
+ else
603
+ /^#{rule_re}/
604
+ end
605
+
606
+ positions = []
607
+ while (rule_start = s.scan_next(rule_start_re))
608
+ pos = s.pos
609
+ positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
610
+ end
611
+ replace_substrings_at(less, positions, &block)
612
+ less
613
+ end
614
+
615
+ # Get a all top-level selectors (with {)
616
+ def get_css_selectors(css, opts = {})
617
+ s = CharStringScanner.new(css)
618
+ selectors = []
619
+ while s.scan_next(RULE_OPEN_BRACE_RE)
620
+ brace_pos = s.pos
621
+ def_pos = css_def_pos(css, brace_pos+1, -1)
622
+ sel = css[def_pos.begin..brace_pos - 1].dup
623
+ sel.strip! if opts[:strip]
624
+ selectors << sel
625
+ sel.dup.strip
626
+ s.pos = close_brace_pos(css, brace_pos, 1) + 1
627
+ end
628
+ selectors
629
+ end
630
+
631
+ # replace in the top-level selector
632
+ # replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
633
+ def replace_in_selector(css, pattern, sub)
634
+ # scan for selector positions in css
635
+ s = CharStringScanner.new(css)
636
+ prev_pos = 0
637
+ sel_pos = []
638
+ while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
639
+ pos = s.pos
640
+ sel_pos << (prev_pos .. pos - 1)
641
+ s.pos = close_brace_pos(css, s.pos - 1) + 1
642
+ prev_pos = pos
643
+ end
644
+ replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
645
+ end
646
+
647
+
648
+ # replace first level properties in the css with yields
649
+ # replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
650
+ def replace_properties(css, &block)
651
+ s = CharStringScanner.new(css)
652
+ s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
653
+ from = s.pos
654
+ m = s.scan_next(/\s*#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}/) || s.scan_next(/\s*#{RULE_CLOSE_BRACE_RE}/)
655
+ to = s.pos - m.length - 1
656
+ replace_substrings_at css, [(from .. to)], &block
657
+ end
658
+
659
+
660
+ # immediate selector of css at pos
661
+ def selector_for_pos(css, pos, depth = -1)
662
+ css[css_def_pos(css, pos, depth)].dup.strip
663
+ end
664
+
665
+ # get the pos of css def at pos (search backwards)
666
+ def css_def_pos(css, pos, depth = -1)
667
+ to = open_brace_pos(css, pos, depth)
668
+ prev_def = to - (css[0..to].reverse.index(RULE_CLOSE_BRACE_RE_REVERSE) || to) + 1
669
+ from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
670
+ (from..to - 1)
671
+ end
672
+
673
+ # next matching brace for brace at from
674
+ def close_brace_pos(css, from, depth = 0)
675
+ s = CharStringScanner.new(css[from..-1])
676
+ while (b = s.scan_next(BRACE_RE))
677
+ depth += (b == '}' ? -1 : +1)
678
+ break if depth.zero?
679
+ end
680
+ raise "match not found for {" unless depth.zero?
681
+ from + s.pos - 1
682
+ end
683
+
684
+ # opening brace position from +from+ (search backwards)
685
+ def open_brace_pos(css, from, depth = 0)
686
+ s = CharStringScanner.new(css[0..from].reverse)
687
+ while (b = s.scan_next(BRACE_RE_REVERSE))
688
+ depth += (b == '{' ? +1 : -1)
689
+ break if depth.zero?
690
+ end
691
+ raise "matching { brace not found" unless depth.zero?
692
+ from - s.pos + 1
693
+ end
694
+
695
+ # insert substitutions into text at positions (Range or Fixnum)
696
+ # substitutions can be passed as array or as yields from the &block called with |substring, position, text|
697
+ # position is a range (begin..end)
698
+ def replace_substrings_at(text, positions, replacements = nil, &block)
699
+ offset = 0
700
+ positions.each_with_index do |p, i|
701
+ p = (p...p) if p.is_a?(Fixnum)
702
+ from = p.begin + offset
703
+ to = p.end + offset
704
+ p = p.exclude_end? ? (from...to) : (from..to)
705
+ # block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
706
+ r = replacements ? replacements[i] : block.call(text[p], p, text)
707
+ text[p] = r
708
+ # add the change in length to offset
709
+ offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
710
+ end
711
+ text
712
+ end
713
+ end
714
+ end