style-sass 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.travis.yml +25 -0
- data/Gemfile +10 -0
- data/LICENSE +21 -0
- data/README.md +195 -0
- data/Rakefile +99 -0
- data/assets/fonts/bootstrap/glyphicons-halflings-regular.eot +0 -0
- data/assets/fonts/bootstrap/glyphicons-halflings-regular.svg +288 -0
- data/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf +0 -0
- data/assets/fonts/bootstrap/glyphicons-halflings-regular.woff +0 -0
- data/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2 +0 -0
- data/assets/images/.keep +0 -0
- data/assets/javascripts/bootstrap-sprockets.js +12 -0
- data/assets/javascripts/bootstrap.js +2363 -0
- data/assets/javascripts/bootstrap.min.js +7 -0
- data/assets/javascripts/bootstrap/affix.js +162 -0
- data/assets/javascripts/bootstrap/alert.js +94 -0
- data/assets/javascripts/bootstrap/button.js +120 -0
- data/assets/javascripts/bootstrap/carousel.js +237 -0
- data/assets/javascripts/bootstrap/collapse.js +211 -0
- data/assets/javascripts/bootstrap/dropdown.js +165 -0
- data/assets/javascripts/bootstrap/modal.js +337 -0
- data/assets/javascripts/bootstrap/popover.js +108 -0
- data/assets/javascripts/bootstrap/scrollspy.js +172 -0
- data/assets/javascripts/bootstrap/tab.js +155 -0
- data/assets/javascripts/bootstrap/tooltip.js +514 -0
- data/assets/javascripts/bootstrap/transition.js +59 -0
- data/assets/stylesheets/_bootstrap-compass.scss +9 -0
- data/assets/stylesheets/_bootstrap-mincer.scss +19 -0
- data/assets/stylesheets/_bootstrap-sprockets.scss +9 -0
- data/assets/stylesheets/_bootstrap.scss +56 -0
- data/assets/stylesheets/_font-awesome.css.scss +2026 -0
- data/assets/stylesheets/_normalize.css.scss +427 -0
- data/assets/stylesheets/bootstrap/_alerts.scss +73 -0
- data/assets/stylesheets/bootstrap/_badges.scss +68 -0
- data/assets/stylesheets/bootstrap/_breadcrumbs.scss +28 -0
- data/assets/stylesheets/bootstrap/_button-groups.scss +244 -0
- data/assets/stylesheets/bootstrap/_buttons.scss +168 -0
- data/assets/stylesheets/bootstrap/_carousel.scss +270 -0
- data/assets/stylesheets/bootstrap/_close.scss +36 -0
- data/assets/stylesheets/bootstrap/_code.scss +69 -0
- data/assets/stylesheets/bootstrap/_component-animations.scss +37 -0
- data/assets/stylesheets/bootstrap/_dropdowns.scss +216 -0
- data/assets/stylesheets/bootstrap/_forms.scss +617 -0
- data/assets/stylesheets/bootstrap/_glyphicons.scss +307 -0
- data/assets/stylesheets/bootstrap/_grid.scss +84 -0
- data/assets/stylesheets/bootstrap/_input-groups.scss +171 -0
- data/assets/stylesheets/bootstrap/_jumbotron.scss +54 -0
- data/assets/stylesheets/bootstrap/_labels.scss +66 -0
- data/assets/stylesheets/bootstrap/_list-group.scss +130 -0
- data/assets/stylesheets/bootstrap/_media.scss +66 -0
- data/assets/stylesheets/bootstrap/_mixins.scss +40 -0
- data/assets/stylesheets/bootstrap/_modals.scss +150 -0
- data/assets/stylesheets/bootstrap/_navbar.scss +662 -0
- data/assets/stylesheets/bootstrap/_navs.scss +242 -0
- data/assets/stylesheets/bootstrap/_normalize.scss +424 -0
- data/assets/stylesheets/bootstrap/_pager.scss +54 -0
- data/assets/stylesheets/bootstrap/_pagination.scss +89 -0
- data/assets/stylesheets/bootstrap/_panels.scss +271 -0
- data/assets/stylesheets/bootstrap/_popovers.scss +131 -0
- data/assets/stylesheets/bootstrap/_print.scss +101 -0
- data/assets/stylesheets/bootstrap/_progress-bars.scss +87 -0
- data/assets/stylesheets/bootstrap/_responsive-embed.scss +35 -0
- data/assets/stylesheets/bootstrap/_responsive-utilities.scss +179 -0
- data/assets/stylesheets/bootstrap/_scaffolding.scss +161 -0
- data/assets/stylesheets/bootstrap/_tables.scss +234 -0
- data/assets/stylesheets/bootstrap/_theme.scss +291 -0
- data/assets/stylesheets/bootstrap/_thumbnails.scss +38 -0
- data/assets/stylesheets/bootstrap/_tooltip.scss +101 -0
- data/assets/stylesheets/bootstrap/_type.scss +298 -0
- data/assets/stylesheets/bootstrap/_utilities.scss +55 -0
- data/assets/stylesheets/bootstrap/_variables.scss +874 -0
- data/assets/stylesheets/bootstrap/_wells.scss +29 -0
- data/assets/stylesheets/bootstrap/mixins/_alerts.scss +14 -0
- data/assets/stylesheets/bootstrap/mixins/_background-variant.scss +12 -0
- data/assets/stylesheets/bootstrap/mixins/_border-radius.scss +18 -0
- data/assets/stylesheets/bootstrap/mixins/_buttons.scss +65 -0
- data/assets/stylesheets/bootstrap/mixins/_center-block.scss +7 -0
- data/assets/stylesheets/bootstrap/mixins/_clearfix.scss +22 -0
- data/assets/stylesheets/bootstrap/mixins/_forms.scss +88 -0
- data/assets/stylesheets/bootstrap/mixins/_gradients.scss +58 -0
- data/assets/stylesheets/bootstrap/mixins/_grid-framework.scss +81 -0
- data/assets/stylesheets/bootstrap/mixins/_grid.scss +122 -0
- data/assets/stylesheets/bootstrap/mixins/_hide-text.scss +21 -0
- data/assets/stylesheets/bootstrap/mixins/_image.scss +33 -0
- data/assets/stylesheets/bootstrap/mixins/_labels.scss +12 -0
- data/assets/stylesheets/bootstrap/mixins/_list-group.scss +32 -0
- data/assets/stylesheets/bootstrap/mixins/_nav-divider.scss +10 -0
- data/assets/stylesheets/bootstrap/mixins/_nav-vertical-align.scss +9 -0
- data/assets/stylesheets/bootstrap/mixins/_opacity.scss +8 -0
- data/assets/stylesheets/bootstrap/mixins/_pagination.scss +24 -0
- data/assets/stylesheets/bootstrap/mixins/_panels.scss +24 -0
- data/assets/stylesheets/bootstrap/mixins/_progress-bar.scss +10 -0
- data/assets/stylesheets/bootstrap/mixins/_reset-filter.scss +8 -0
- data/assets/stylesheets/bootstrap/mixins/_reset-text.scss +18 -0
- data/assets/stylesheets/bootstrap/mixins/_resize.scss +6 -0
- data/assets/stylesheets/bootstrap/mixins/_responsive-visibility.scss +21 -0
- data/assets/stylesheets/bootstrap/mixins/_size.scss +10 -0
- data/assets/stylesheets/bootstrap/mixins/_tab-focus.scss +9 -0
- data/assets/stylesheets/bootstrap/mixins/_table-row.scss +28 -0
- data/assets/stylesheets/bootstrap/mixins/_text-emphasis.scss +12 -0
- data/assets/stylesheets/bootstrap/mixins/_text-overflow.scss +8 -0
- data/assets/stylesheets/bootstrap/mixins/_vendor-prefixes.scss +222 -0
- data/lib/styler-sass.rb +94 -0
- data/lib/styler-sass/engine.rb +17 -0
- data/lib/styler-sass/version.rb +3 -0
- data/styler-sass.gemspec +48 -0
- data/tasks/bower.rake +31 -0
- data/tasks/converter.rb +80 -0
- data/tasks/converter/char_string_scanner.rb +38 -0
- data/tasks/converter/fonts_conversion.rb +16 -0
- data/tasks/converter/js_conversion.rb +47 -0
- data/tasks/converter/less_conversion.rb +714 -0
- data/tasks/converter/logger.rb +57 -0
- data/tasks/converter/network.rb +97 -0
- data/templates/project/_bootstrap-variables.sass +875 -0
- data/templates/project/manifest.rb +20 -0
- data/templates/project/styles.sass +6 -0
- metadata +449 -0
data/lib/styler-sass.rb
ADDED
@@ -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
|
data/styler-sass.gemspec
ADDED
@@ -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
|
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
|
data/tasks/converter.rb
ADDED
@@ -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
|