2013-07-30 22:08:49 +02:00
|
|
|
# coding: utf-8
|
2013-05-23 14:51:56 -07:00
|
|
|
# Based on convert script from vwall/compass-twitter-bootstrap gem.
|
|
|
|
# https://github.com/vwall/compass-twitter-bootstrap/blob/master/build/convert.rb
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this work except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License in the LICENSE file, or at:
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
require 'open-uri'
|
|
|
|
require 'json'
|
2013-07-31 02:27:29 +02:00
|
|
|
require 'strscan'
|
2013-08-04 23:22:54 +02:00
|
|
|
require 'forwardable'
|
|
|
|
require 'term/ansicolor'
|
2013-08-05 00:44:05 +02:00
|
|
|
require 'fileutils'
|
2013-05-23 14:51:56 -07:00
|
|
|
|
|
|
|
class Converter
|
2013-08-04 23:22:54 +02:00
|
|
|
extend Forwardable
|
|
|
|
|
2013-08-03 09:22:54 +02:00
|
|
|
GIT_DATA = 'https://api.github.com/repos'
|
2013-08-04 23:22:54 +02:00
|
|
|
GIT_RAW = 'https://raw.github.com'
|
|
|
|
|
2013-06-13 13:58:34 -07:00
|
|
|
def initialize(branch)
|
2013-08-03 09:22:54 +02:00
|
|
|
@repo = 'twbs/bootstrap'
|
2013-08-03 09:31:57 +02:00
|
|
|
@repo_url = "https://github.com/#@repo"
|
2013-08-03 09:22:54 +02:00
|
|
|
@branch = branch || 'master'
|
|
|
|
@branch_sha = get_branch_sha
|
2013-08-20 16:07:44 +02:00
|
|
|
@save_at = { js: 'vendor/assets/javascripts/bootstrap',
|
|
|
|
scss: 'vendor/assets/stylesheets/bootstrap',
|
|
|
|
fonts: 'vendor/assets/fonts/bootstrap' }
|
2013-08-05 00:44:05 +02:00
|
|
|
@save_at.each { |_,v| FileUtils.mkdir_p(v) }
|
2013-08-18 20:13:13 +02:00
|
|
|
@cache_path = 'tmp/converter-cache'
|
|
|
|
@logger = Logger.new(repo: @repo_url, branch: @branch, branch_sha: @branch_sha, save_at: @save_at, cache_path: @cache_path)
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def_delegators :@logger, :log_status, :log_processing, :log_transform, :log_file_info, :log_processed, :log_http_get_file, :log_http_get_files, :silence_log
|
2013-08-04 23:22:54 +02:00
|
|
|
|
2013-05-23 14:51:56 -07:00
|
|
|
def process
|
2013-05-24 16:59:48 -07:00
|
|
|
process_stylesheet_assets
|
|
|
|
process_javascript_assets
|
2013-08-20 16:07:44 +02:00
|
|
|
process_font_assets
|
2013-08-03 09:22:54 +02:00
|
|
|
store_version
|
2013-05-24 16:59:48 -07:00
|
|
|
end
|
|
|
|
|
2013-08-20 16:07:44 +02:00
|
|
|
def process_font_assets
|
|
|
|
log_status "Processing fonts..."
|
|
|
|
files = read_files('fonts', bootstrap_font_files)
|
|
|
|
save_at = @save_at[:fonts]
|
|
|
|
files.each do |name, content|
|
|
|
|
save_file "#{save_at}/#{name}", content
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-18 23:00:27 +02:00
|
|
|
NESTED_MIXINS = {'#gradient' => 'gradient'}
|
2013-08-09 02:44:00 +02:00
|
|
|
VARARG_MIXINS = %w(transition transition-transform box-shadow)
|
2013-05-24 16:59:48 -07:00
|
|
|
def process_stylesheet_assets
|
2013-08-04 23:22:54 +02:00
|
|
|
log_status "Processing stylesheets..."
|
2013-08-05 02:22:14 +02:00
|
|
|
files = read_files('less', bootstrap_less_files)
|
2013-08-18 23:00:27 +02:00
|
|
|
|
2013-08-20 18:30:06 +02:00
|
|
|
# read common mixin definitions (incl. nested mixins) from mixins.less
|
|
|
|
read_shared_mixins! files['mixins.less']
|
2013-08-18 23:00:27 +02:00
|
|
|
|
|
|
|
# convert each file
|
2013-08-05 02:22:14 +02:00
|
|
|
files.each do |name, file|
|
2013-08-05 00:21:17 +02:00
|
|
|
log_processing name
|
2013-08-20 18:30:06 +02:00
|
|
|
# apply common conversions
|
|
|
|
file = convert_to_scss(file)
|
2013-06-15 16:30:44 -07:00
|
|
|
case name
|
|
|
|
when 'mixins.less'
|
2013-08-18 23:00:27 +02:00
|
|
|
NESTED_MIXINS.each do |selector, prefix|
|
|
|
|
file = flatten_mixins(file, selector, prefix)
|
|
|
|
end
|
2013-08-09 02:44:00 +02:00
|
|
|
file = varargify_mixin_definitions(file, *VARARG_MIXINS)
|
|
|
|
file = deinterpolate_vararg_mixins(file)
|
2013-08-14 01:42:45 +02:00
|
|
|
file = parameterize_mixin_parent_selector file, 'responsive-(in)?visibility'
|
|
|
|
file = parameterize_mixin_parent_selector file, 'input-size'
|
2013-08-04 23:37:34 +02:00
|
|
|
file = replace_ms_filters(file)
|
2013-08-15 16:08:46 +02:00
|
|
|
file = replace_all file, /\.\$state/, '.#{$state}'
|
2013-08-20 15:38:26 +02:00
|
|
|
file = replace_all file, /,\s*\.open \.dropdown-toggle& \{(.*?)\}/m,
|
|
|
|
" {\\1}\n .open & { &.dropdown-toggle {\\1} }"
|
2013-07-31 18:02:23 +02:00
|
|
|
when 'responsive-utilities.less'
|
2013-08-20 15:07:13 +02:00
|
|
|
file = apply_mixin_parent_selector(file, '&\.(visible|hidden)')
|
2013-08-20 17:54:23 +02:00
|
|
|
file = apply_mixin_parent_selector(file, '(?<!&)\.(visible|hidden)')
|
2013-08-12 15:24:51 +02:00
|
|
|
file = replace_rules(file, ' @media') { |r| unindent(r, 2) }
|
2013-06-15 16:30:44 -07:00
|
|
|
when 'variables.less'
|
|
|
|
file = insert_default_vars(file)
|
2013-08-20 16:17:16 +02:00
|
|
|
file = replace_all file, /(\$icon-font-path:).*(!default)/, '\1 "bootstrap/" \2'
|
2013-07-31 18:45:17 +02:00
|
|
|
when 'close.less'
|
|
|
|
# extract .close { button& {...} } rule
|
2013-08-14 17:38:43 +02:00
|
|
|
file = extract_nested_rule file, 'button&'
|
2013-08-20 15:38:26 +02:00
|
|
|
when 'modals.less'
|
|
|
|
file = replace_all file, /body&,(.*?)(\{.*?\})/m, "\\1\\2\nbody& \\2"
|
|
|
|
file = extract_nested_rule file, 'body&'
|
|
|
|
when 'dropdowns.less'
|
|
|
|
file = replace_all file, /(\s*)@extend \.pull-right-dropdown-menu;/, "\\1right: 0;\\1left: auto;"
|
2013-08-01 01:29:45 +02:00
|
|
|
when 'forms.less'
|
2013-08-14 17:38:43 +02:00
|
|
|
file = extract_nested_rule file, 'textarea&'
|
2013-08-14 01:42:45 +02:00
|
|
|
file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
|
|
|
|
when 'navbar.less'
|
2013-08-14 16:21:26 +02:00
|
|
|
file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
|
2013-08-15 16:08:46 +02:00
|
|
|
when 'tables.less'
|
|
|
|
file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
|
2013-08-12 15:24:51 +02:00
|
|
|
when 'list-group.less'
|
2013-08-14 17:38:43 +02:00
|
|
|
file = extract_nested_rule file, 'a&'
|
2013-08-20 15:07:13 +02:00
|
|
|
when 'glyphicons.less'
|
|
|
|
file = replace_rules(file, '@font-face') { |rule|
|
2013-08-21 13:46:35 +02:00
|
|
|
rule = replace_all rule, /(\$icon-font-\w+)/, '#{\1}'
|
2013-08-20 15:07:13 +02:00
|
|
|
replace_all rule, /url\(/, 'font-url('
|
|
|
|
}
|
2013-06-15 16:30:44 -07:00
|
|
|
end
|
|
|
|
|
2013-08-05 00:44:05 +02:00
|
|
|
name = name.sub(/\.less$/, '.scss')
|
|
|
|
save_at = @save_at[:scss]
|
|
|
|
path = "#{save_at}/#{'_' unless name == 'bootstrap.scss'}#{name}"
|
2013-06-15 16:30:44 -07:00
|
|
|
save_file(path, file)
|
2013-08-05 00:44:05 +02:00
|
|
|
log_processed File.basename(path)
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-03 09:22:54 +02:00
|
|
|
def store_version
|
|
|
|
path = 'lib/bootstrap-sass/version.rb'
|
2013-08-04 03:55:35 +02:00
|
|
|
content = File.read(path).sub(/BOOTSTRAP_SHA\s*=\s*['"][\w]+['"]/, "BOOTSTRAP_SHA = '#@branch_sha'")
|
2013-08-03 09:22:54 +02:00
|
|
|
File.open(path, 'w') { |f| f.write(content) }
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def process_javascript_assets
|
2013-08-04 23:22:54 +02:00
|
|
|
log_status "Processing javascripts..."
|
2013-08-05 00:44:05 +02:00
|
|
|
save_at = @save_at[:js]
|
2013-07-30 22:08:49 +02:00
|
|
|
read_files('js', bootstrap_js_files).each do |name, file|
|
2013-08-05 00:21:17 +02:00
|
|
|
save_file("#{save_at}/#{name}", file)
|
2013-05-24 16:59:48 -07:00
|
|
|
end
|
2013-08-05 00:44:05 +02:00
|
|
|
log_processed "#{bootstrap_js_files * ' '}"
|
2013-05-24 16:59:48 -07:00
|
|
|
|
2013-08-05 01:46:33 +02:00
|
|
|
log_status "Updating javascript manifest"
|
2013-05-24 16:59:48 -07:00
|
|
|
content = ''
|
|
|
|
bootstrap_js_files.each do |name|
|
|
|
|
name = name.gsub(/\.js$/, '')
|
|
|
|
content << "//= require bootstrap/#{name}\n"
|
|
|
|
end
|
|
|
|
path = "vendor/assets/javascripts/bootstrap.js"
|
|
|
|
save_file(path, content)
|
2013-08-05 00:21:17 +02:00
|
|
|
log_processed path
|
2013-05-24 16:59:48 -07:00
|
|
|
end
|
|
|
|
|
2013-08-03 00:22:57 +02:00
|
|
|
private
|
2013-05-23 14:51:56 -07:00
|
|
|
|
2013-07-30 22:08:49 +02:00
|
|
|
def read_files(path, files)
|
2013-08-03 09:22:54 +02:00
|
|
|
full_path = "#{GIT_RAW}/#@repo/#@branch_sha/#{path}"
|
2013-08-18 20:13:13 +02:00
|
|
|
if (contents = read_cached_files(path, files))
|
2013-08-20 20:09:56 +02:00
|
|
|
log_http_get_files files, full_path, true
|
2013-08-18 20:13:13 +02:00
|
|
|
else
|
2013-08-20 20:09:56 +02:00
|
|
|
log_http_get_files files, full_path, false
|
2013-08-18 20:13:13 +02:00
|
|
|
contents = {}
|
|
|
|
files.map do |name|
|
|
|
|
Thread.start {
|
|
|
|
content = open("#{full_path}/#{name}").read
|
|
|
|
Thread.exclusive { contents[name] = content }
|
2013-07-30 22:08:49 +02:00
|
|
|
}
|
2013-08-18 20:13:13 +02:00
|
|
|
end.each(&:join)
|
|
|
|
write_cached_files path, contents
|
|
|
|
end
|
2013-07-30 22:08:49 +02:00
|
|
|
contents
|
|
|
|
end
|
|
|
|
|
2013-08-18 20:13:13 +02:00
|
|
|
def read_cached_files(path, files)
|
|
|
|
full_path = "#@cache_path/#@branch_sha/#{path}"
|
|
|
|
contents = {}
|
|
|
|
if File.directory?(full_path)
|
|
|
|
files.each do |name|
|
2013-08-20 16:07:44 +02:00
|
|
|
contents[name] = File.read("#{full_path}/#{name}", mode: 'rb') || ''
|
2013-08-18 20:13:13 +02:00
|
|
|
end
|
|
|
|
contents
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def write_cached_files(path, files)
|
|
|
|
full_path = "./#@cache_path/#@branch_sha/#{path}"
|
|
|
|
FileUtils.mkdir_p full_path
|
|
|
|
files.each do |name, content|
|
2013-08-20 16:07:44 +02:00
|
|
|
File.open("#{full_path}/#{name}", 'wb') { |f| f.write content}
|
2013-08-18 20:13:13 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-20 15:07:13 +02:00
|
|
|
|
|
|
|
def get_file(url)
|
|
|
|
cache_path = "./#@cache_path#{URI(url).path}"
|
|
|
|
FileUtils.mkdir_p File.dirname(cache_path)
|
|
|
|
if File.exists?(cache_path)
|
2013-08-20 20:09:56 +02:00
|
|
|
log_http_get_file url, true
|
2013-08-20 16:07:44 +02:00
|
|
|
File.read(cache_path, mode: 'rb')
|
2013-08-20 15:07:13 +02:00
|
|
|
else
|
2013-08-20 20:09:56 +02:00
|
|
|
log_http_get_file url, false
|
2013-08-20 15:07:13 +02:00
|
|
|
content = open(url).read
|
2013-08-20 16:07:44 +02:00
|
|
|
File.open(cache_path, 'wb') { |f| f.write content }
|
2013-08-20 15:07:13 +02:00
|
|
|
content
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-03 09:22:54 +02:00
|
|
|
# get sha of the branch (= the latest commit)
|
|
|
|
def get_branch_sha
|
2013-08-20 15:07:13 +02:00
|
|
|
cmd = "git ls-remote '#@repo_url' | awk '/#@branch/ {print $1}'"
|
|
|
|
puts cmd
|
|
|
|
@branch_sha ||= %x[#{cmd}].chomp
|
2013-08-20 15:38:26 +02:00
|
|
|
raise 'Could not get branch sha!' unless $?.success?
|
|
|
|
@branch_sha
|
2013-08-03 09:22:54 +02:00
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
# Get the sha of a dir
|
|
|
|
def get_tree_sha(dir)
|
2013-08-03 09:22:54 +02:00
|
|
|
get_trees['tree'].find { |t| t['path'] == dir }['sha']
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_trees
|
2013-08-03 09:31:57 +02:00
|
|
|
@trees ||= get_json("#{GIT_DATA}/#@repo/git/trees/#@branch_sha")
|
2013-05-24 16:59:48 -07:00
|
|
|
end
|
|
|
|
|
2013-08-20 16:07:44 +02:00
|
|
|
def bootstrap_font_files
|
|
|
|
@bootstrap_font_files ||= begin
|
|
|
|
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('fonts')}"
|
|
|
|
files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.(eot|svg|ttf|woff)$/ }.map { |f| f['path'] }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def bootstrap_less_files
|
2013-07-30 22:08:49 +02:00
|
|
|
@bootstrap_less_files ||= begin
|
2013-08-03 09:31:57 +02:00
|
|
|
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('less')}"
|
2013-08-20 16:07:44 +02:00
|
|
|
files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.less$/ }.map { |f| f['path'] }
|
2013-07-30 22:08:49 +02:00
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def bootstrap_js_files
|
2013-07-30 22:08:49 +02:00
|
|
|
@bootstrap_js_files ||= begin
|
2013-08-03 09:31:57 +02:00
|
|
|
files = get_json "#{GIT_DATA}/#@repo/git/trees/#{get_tree_sha('js')}"
|
2013-08-20 16:07:44 +02:00
|
|
|
files = files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /\.js$/ }.map { |f| f['path'] }
|
2013-07-30 22:08:49 +02:00
|
|
|
files.sort_by { |f|
|
|
|
|
case f
|
|
|
|
# tooltip depends on popover and must be loaded earlier
|
2013-08-03 09:31:57 +02:00
|
|
|
when /tooltip/ then 1
|
|
|
|
when /popover/ then 2
|
2013-07-30 22:08:49 +02:00
|
|
|
else
|
|
|
|
0
|
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-08-20 18:36:13 +02:00
|
|
|
# We need to keep a list of shared mixin names in order to convert the includes correctly
|
|
|
|
# Before doing any processing we read shared mixins from a file
|
|
|
|
# If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
|
2013-08-20 18:30:06 +02:00
|
|
|
def read_shared_mixins!(mixins_file)
|
2013-08-20 20:09:56 +02:00
|
|
|
log_status " Reading shared mixins from mixins.less"
|
2013-08-20 21:12:25 +02:00
|
|
|
@shared_mixins = get_mixin_names(mixins_file, silent: true)
|
2013-08-20 18:30:06 +02:00
|
|
|
NESTED_MIXINS.each do |selector, prefix|
|
2013-08-20 18:36:13 +02:00
|
|
|
# we use replace_rules without replacing anything just to use the parsing algorithm
|
2013-08-20 18:30:06 +02:00
|
|
|
replace_rules(mixins_file, selector) { |rule|
|
2013-08-20 21:12:25 +02:00
|
|
|
@shared_mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
|
2013-08-20 18:30:06 +02:00
|
|
|
rule
|
|
|
|
}
|
|
|
|
end
|
2013-08-20 21:12:25 +02:00
|
|
|
@shared_mixins.sort!
|
|
|
|
log_file_info "shared mixins: #{@shared_mixins * ', '}"
|
|
|
|
@shared_mixins
|
2013-08-20 18:30:06 +02:00
|
|
|
end
|
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def get_mixin_names(file, opts = {})
|
2013-08-20 21:12:25 +02:00
|
|
|
names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)[ ]*\{/).map(&:first).uniq.sort
|
2013-08-20 20:09:56 +02:00
|
|
|
log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
|
|
|
|
names
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def convert_to_scss(file)
|
2013-08-20 18:36:13 +02:00
|
|
|
# mixins may also be defined in the file. get mixin names before doing any processing
|
2013-08-20 21:12:25 +02:00
|
|
|
mixin_names = (@shared_mixins + get_mixin_names(file)).uniq
|
2013-05-23 14:51:56 -07:00
|
|
|
file = replace_vars(file)
|
2013-08-20 18:30:06 +02:00
|
|
|
file = replace_file_imports(file)
|
|
|
|
file = replace_mixin_definitions file
|
|
|
|
file = replace_mixins file, mixin_names
|
|
|
|
# replace_less_extend does not seem to do anything. @glebm
|
2013-05-23 14:51:56 -07:00
|
|
|
file = replace_less_extend(file)
|
|
|
|
file = replace_spin(file)
|
|
|
|
file = replace_image_urls(file)
|
|
|
|
file = replace_image_paths(file)
|
|
|
|
file = replace_escaping(file)
|
|
|
|
file = convert_less_ampersand(file)
|
2013-08-09 02:44:00 +02:00
|
|
|
file = deinterpolate_vararg_mixins(file)
|
2013-08-21 16:19:37 +02:00
|
|
|
file = replace_calculation_semantics(file)
|
2013-05-23 14:51:56 -07:00
|
|
|
file
|
|
|
|
end
|
|
|
|
|
2013-08-21 16:19:37 +02:00
|
|
|
# margin: a -b
|
|
|
|
# LESS: sets 2 values
|
|
|
|
# SASS: sets 1 value (a-b)
|
|
|
|
# This wraps a and -b so they evaluates to 2 values in SASS
|
|
|
|
def replace_calculation_semantics(file)
|
|
|
|
# split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
|
|
|
|
# #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
|
|
|
|
split_prop_val = proc { |val|
|
|
|
|
s = CharStringScanner.new(val)
|
|
|
|
r = []
|
|
|
|
buff = ''
|
|
|
|
d = 0
|
|
|
|
prop_char = %r([\$\w\-/\*\+%!])
|
|
|
|
while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
|
|
|
|
buff << token
|
|
|
|
case token
|
|
|
|
when '('
|
|
|
|
d += 1
|
|
|
|
when ')'
|
|
|
|
d -= 1
|
|
|
|
if d == 0
|
|
|
|
r << buff
|
|
|
|
buff = ''
|
|
|
|
end
|
|
|
|
when /\s/
|
|
|
|
if d == 0 && !buff.strip.empty?
|
|
|
|
r << buff
|
|
|
|
buff = ''
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
r << buff unless buff.empty?
|
|
|
|
r.map(&:strip)
|
|
|
|
}
|
|
|
|
|
|
|
|
replace_rules file do |rule|
|
|
|
|
replace_properties rule do |props|
|
|
|
|
props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
|
|
|
|
prop, vals = $1, split_prop_val.call($2)
|
2013-08-21 16:24:46 +02:00
|
|
|
next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
|
2013-08-21 16:19:37 +02:00
|
|
|
transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
|
|
|
|
log_transform "property #{prop}: #{transformed * ' '}"
|
|
|
|
"#{prop}: #{transformed * ' '};"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def save_file(path, content, mode='w')
|
|
|
|
File.open(path, mode) { |file| file.write(content) }
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-08-05 02:43:17 +02:00
|
|
|
# @import "file.less" to "#{target_path}file;"
|
|
|
|
def replace_file_imports(less, target_path = 'bootstrap/')
|
2013-08-18 20:17:01 +02:00
|
|
|
less.gsub %r([@\$]import ["|']([\w-]+).less["|'];),
|
2013-08-05 02:43:17 +02:00
|
|
|
%Q(@import "#{target_path}\\1";)
|
2013-06-15 16:30:44 -07:00
|
|
|
end
|
|
|
|
|
2013-08-14 01:42:45 +02:00
|
|
|
def replace_all(file, regex, replacement = nil, &block)
|
|
|
|
log_transform regex, replacement
|
2013-08-18 23:00:27 +02:00
|
|
|
new_file = file.gsub(regex, replacement, &block)
|
|
|
|
raise "replace_all #{regex}, #{replacement} NO MATCH" if file == new_file
|
|
|
|
new_file
|
2013-08-14 01:42:45 +02:00
|
|
|
end
|
|
|
|
|
2013-07-31 17:04:30 +02:00
|
|
|
# @mixin a() { tr& { color:white } }
|
|
|
|
# to:
|
|
|
|
# @mixin a($parent) { tr#{$parent} { color: white } }
|
|
|
|
def parameterize_mixin_parent_selector(file, rule_sel)
|
2013-08-04 23:37:34 +02:00
|
|
|
log_transform rule_sel
|
2013-07-31 17:04:30 +02:00
|
|
|
param = '$parent'
|
2013-08-03 00:22:57 +02:00
|
|
|
replace_rules(file, '^[ \t]*@mixin\s*' + rule_sel) do |mxn_css|
|
|
|
|
mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
|
|
|
|
# insert param into mixin def
|
2013-08-14 01:42:45 +02:00
|
|
|
mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
|
2013-08-03 00:22:57 +02:00
|
|
|
# wrap properties in #{$parent} { ... }
|
2013-09-29 00:05:08 +02:00
|
|
|
replace_properties(mxn_css) { |props| props.strip.empty? ? props : " \#{#{param}} { #{props.strip} }\n " }
|
2013-08-03 00:22:57 +02:00
|
|
|
# change nested& rules to nested#{$parent}
|
2013-09-29 00:05:08 +02:00
|
|
|
replace_rules(mxn_css, /.*&[ ,]/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-14 17:38:43 +02:00
|
|
|
# extracts rule immediately after it's parent, and adjust the selector
|
|
|
|
# .x { textarea& { ... }}
|
|
|
|
# to:
|
|
|
|
# .x { ... }
|
|
|
|
# textarea.x { ... }
|
2013-08-20 15:07:13 +02:00
|
|
|
def extract_nested_rule(file, selector, new_selector = nil)
|
2013-08-03 00:22:57 +02:00
|
|
|
matches = []
|
|
|
|
# first find the rules, and remove them
|
2013-08-20 15:07:13 +02:00
|
|
|
file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
|
2013-08-03 00:22:57 +02:00
|
|
|
matches << [rule, pos]
|
2013-08-14 17:38:43 +02:00
|
|
|
new_selector ||= "#{get_selector(rule).sub(/&$/, '')}#{selector_for_pos(css, pos.begin)}"
|
2013-08-03 00:22:57 +02:00
|
|
|
indent "// [converter] extracted #{get_selector(rule)} to #{new_selector}", indent_width(rule)
|
|
|
|
}
|
2013-08-14 17:38:43 +02:00
|
|
|
log_transform selector, new_selector
|
2013-07-31 19:10:50 +02:00
|
|
|
# replace rule selector with new_selector
|
2013-08-03 00:22:57 +02:00
|
|
|
matches.each do |m|
|
|
|
|
m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{new_selector}\\3{"
|
|
|
|
end
|
2013-08-20 15:07:13 +02:00
|
|
|
replace_substrings_at file,
|
|
|
|
matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
|
2013-08-03 00:22:57 +02:00
|
|
|
matches.map { |rule, _| "\n\n" + unindent(rule) }
|
2013-07-31 19:10:50 +02:00
|
|
|
end
|
|
|
|
|
2013-07-31 18:06:22 +02:00
|
|
|
# .visible-sm { @include responsive-visibility() }
|
|
|
|
# to:
|
|
|
|
# @include responsive-visibility('.visible-sm')
|
|
|
|
def apply_mixin_parent_selector(file, rule_sel)
|
2013-08-04 23:37:34 +02:00
|
|
|
log_transform rule_sel
|
2013-08-20 15:07:13 +02:00
|
|
|
replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
|
|
|
|
body = unwrap_rule_block(rule.dup).strip
|
|
|
|
next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
|
|
|
|
rule =~ /(#{COMMENT_RE}*)(#{SELECTOR_RE})\{/
|
2013-08-14 01:42:45 +02:00
|
|
|
cmt, sel = $1, $2.strip
|
2013-08-20 15:07:13 +02:00
|
|
|
# take one up selector chain if this is an &. selector
|
|
|
|
if sel.start_with?('&')
|
|
|
|
parent_sel = selector_for_pos(css, rule_pos.begin)
|
|
|
|
sel = parent_sel + sel[1..-1]
|
|
|
|
end
|
|
|
|
# unwrap, and replace @include
|
|
|
|
unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(([\$\w\-,\s]*)\)/) {
|
|
|
|
"#{cmt}#{$1}('#{sel}'#{', ' if $2 && !$2.empty?}#{$2})"
|
|
|
|
}
|
2013-07-31 18:06:22 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-07-31 02:27:29 +02:00
|
|
|
# #gradient > { @mixin horizontal ... }
|
|
|
|
# to:
|
|
|
|
# @mixin gradient-horizontal
|
2013-07-31 18:45:17 +02:00
|
|
|
def flatten_mixins(file, container, prefix)
|
2013-08-04 23:37:34 +02:00
|
|
|
log_transform container, prefix
|
2013-07-31 16:02:19 +02:00
|
|
|
replace_rules file, Regexp.escape(container) do |mixins_css|
|
2013-08-03 00:22:57 +02:00
|
|
|
unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
|
2013-07-31 02:27:29 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-28 20:50:03 -07:00
|
|
|
# Replaces the following:
|
2013-08-01 01:29:45 +02:00
|
|
|
# .mixin() -> @include mixin()
|
|
|
|
# #scope > .mixin() -> @include scope-mixin()
|
2013-08-20 20:09:56 +02:00
|
|
|
def replace_mixins(less, mixin_names)
|
2013-08-20 15:38:26 +02:00
|
|
|
mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))(?!\s\{)/
|
2013-08-18 23:00:27 +02:00
|
|
|
|
2013-05-28 20:50:03 -07:00
|
|
|
less.gsub(mixin_pattern) do |match|
|
|
|
|
matches = match.scan(mixin_pattern).flatten
|
2013-08-04 23:22:54 +02:00
|
|
|
scope = matches[1] || ''
|
2013-05-28 20:50:03 -07:00
|
|
|
if scope != ''
|
2013-07-16 23:29:41 -07:00
|
|
|
scope = scope.scan(/[\w-]+/).join('-') + '-'
|
2013-05-28 20:50:03 -07:00
|
|
|
end
|
2013-08-03 00:22:57 +02:00
|
|
|
mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?\{?/).first
|
2013-08-20 20:09:56 +02:00
|
|
|
if mixin_name && mixin_names.include?("#{scope}#{mixin_name.first}")
|
2013-08-19 02:18:52 +02:00
|
|
|
"#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/, ", $").sub(/;\)$/, ')')
|
2013-07-28 18:14:12 +02:00
|
|
|
else
|
|
|
|
"#{matches.first}@extend .#{scope}#{matches.last.gsub(/\(\)/, '')}"
|
|
|
|
end
|
2013-05-28 20:50:03 -07:00
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-08-04 23:37:34 +02:00
|
|
|
# change Microsoft filters to SASS calling convention
|
|
|
|
def replace_ms_filters(file)
|
|
|
|
log_transform
|
|
|
|
file.gsub(
|
|
|
|
/filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)",argb\(([\-$\w]+)\),argb\(([\-$\w]+)\)\)\);/,
|
|
|
|
%Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2013-08-01 01:58:26 +02:00
|
|
|
# unwraps topmost rule block
|
|
|
|
# #sel { a: b; }
|
|
|
|
# to:
|
|
|
|
# a: b;
|
2013-08-01 02:01:02 +02:00
|
|
|
def unwrap_rule_block(css)
|
2013-08-20 15:07:13 +02:00
|
|
|
css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
|
2013-08-01 01:58:26 +02:00
|
|
|
end
|
|
|
|
|
2013-08-05 02:43:17 +02:00
|
|
|
def replace_mixin_definitions(less)
|
2013-07-30 18:02:33 +02:00
|
|
|
less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
|
2013-08-15 16:08:46 +02:00
|
|
|
"#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
|
2013-07-30 18:02:33 +02:00
|
|
|
}
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def replace_vars(less)
|
2013-08-20 18:30:06 +02:00
|
|
|
less = less.dup
|
|
|
|
# skip header comment
|
|
|
|
less =~ %r(\A/\*(.*?)\*/)m
|
|
|
|
from = $~ ? $~.to_s.length : 0
|
|
|
|
less[from..-1] = less[from..-1].
|
|
|
|
gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
|
|
|
|
# variables that would be ignored by gsub above: e.g. @page-header-border-color
|
|
|
|
gsub(/@(page[\w-]+)/, '$\1')
|
2013-07-30 17:07:44 +02:00
|
|
|
less
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-08-01 01:29:45 +02:00
|
|
|
# #gradient > .horizontal()
|
|
|
|
# to:
|
|
|
|
# @include .horizontal-gradient()
|
2013-05-23 14:51:56 -07:00
|
|
|
def replace_less_extend(less)
|
|
|
|
less.gsub(/\#(\w+) \> \.([\w-]*)(\(.*\));?/, '@include \1-\2\3;')
|
|
|
|
end
|
|
|
|
|
|
|
|
def replace_spin(less)
|
2013-08-01 16:02:46 +02:00
|
|
|
less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def replace_image_urls(less)
|
|
|
|
less.gsub(/background-image: url\("?(.*?)"?\);/) {|s| "background-image: image-url(\"#{$1}\");" }
|
|
|
|
end
|
|
|
|
|
|
|
|
def replace_image_paths(less)
|
|
|
|
less.gsub('../img/', '')
|
|
|
|
end
|
|
|
|
|
|
|
|
def replace_escaping(less)
|
2013-06-13 12:09:18 -07:00
|
|
|
less = less.gsub(/\~"([^"]+)"/, '#{\1}') # Get rid of ~"" escape
|
2013-08-03 01:58:43 +02:00
|
|
|
less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
|
|
|
|
less.gsub!(/"([^"\n]*)(\$[\w\-]+)([^"\n]*)"/, '"\1#{\2}\3"') # interpolate variable in string, e.g. url("$file-1x") => url("#{$file-1x}")
|
2013-07-28 18:21:05 +02:00
|
|
|
less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def insert_default_vars(scss)
|
2013-08-05 03:00:18 +02:00
|
|
|
log_transform
|
2013-06-13 12:29:03 -07:00
|
|
|
scss.gsub(/^(\$.+);/, '\1 !default;')
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
# Converts &-
|
|
|
|
def convert_less_ampersand(less)
|
|
|
|
regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
|
|
|
|
|
|
|
|
tmp = ''
|
|
|
|
less.scan(/^(\s*&)(-[\w\[\]]+\s*{.+})$/) do |ampersand, css|
|
|
|
|
tmp << ".badge#{css}\n"
|
|
|
|
end
|
|
|
|
|
|
|
|
less.gsub(regx, tmp)
|
|
|
|
end
|
2013-07-31 02:27:29 +02:00
|
|
|
|
|
|
|
# unindent by n spaces
|
|
|
|
def unindent(txt, n = 2)
|
2013-08-03 00:22:57 +02:00
|
|
|
txt.gsub /^[ ]{#{n}}/, ''
|
|
|
|
end
|
|
|
|
|
|
|
|
# indent by n spaces
|
|
|
|
def indent(txt, n = 2)
|
|
|
|
"#{' ' * n}#{txt}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# get indent length from the first line of txt
|
|
|
|
def indent_width(txt)
|
|
|
|
txt.match(/\A\s*/).to_s.length
|
|
|
|
end
|
|
|
|
|
2013-08-09 02:44:00 +02:00
|
|
|
# @mixin transition($transition) {
|
|
|
|
# to:
|
|
|
|
# @mixin transition($transition...) {
|
|
|
|
def varargify_mixin_definitions(scss, *mixins)
|
|
|
|
log_transform *mixins
|
|
|
|
scss = scss.dup
|
|
|
|
mixins.each do |mixin|
|
2013-08-10 17:10:01 +02:00
|
|
|
scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
|
2013-08-09 02:44:00 +02:00
|
|
|
end
|
|
|
|
scss
|
|
|
|
end
|
|
|
|
|
|
|
|
# @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
|
|
|
|
# to
|
|
|
|
# @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
|
|
|
|
def deinterpolate_vararg_mixins(scss)
|
|
|
|
scss = scss.dup
|
|
|
|
VARARG_MIXINS.each do |mixin|
|
2013-08-10 17:10:01 +02:00
|
|
|
if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
|
|
|
|
log_transform mixin
|
|
|
|
end
|
2013-08-09 02:44:00 +02:00
|
|
|
end
|
|
|
|
scss
|
|
|
|
end
|
|
|
|
|
2013-08-03 00:22:57 +02:00
|
|
|
# get full selector for rule_block
|
|
|
|
def get_selector(rule_block)
|
|
|
|
/^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
|
2013-07-31 02:27:29 +02:00
|
|
|
end
|
|
|
|
|
2013-08-01 01:58:26 +02:00
|
|
|
# replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
|
|
|
|
# will also include immediately preceding comments in rule_block
|
|
|
|
#
|
|
|
|
# option :comments -- include immediately preceding comments in rule_block
|
2013-07-31 02:27:29 +02:00
|
|
|
#
|
2013-08-09 02:44:00 +02:00
|
|
|
# replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
|
2013-08-20 15:07:13 +02:00
|
|
|
def replace_rules(less, rule_prefix = SELECTOR_RE, options = {}, &block)
|
2013-08-01 01:58:26 +02:00
|
|
|
options = {comments: true}.merge(options || {})
|
2013-08-03 00:22:57 +02:00
|
|
|
less = less.dup
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(less)
|
2013-08-20 15:07:13 +02:00
|
|
|
rule_re = /(?:#{rule_prefix}[^{]*#{RULE_OPEN_BRACE_RE})/
|
2013-08-01 01:58:26 +02:00
|
|
|
if options[:comments]
|
2013-08-20 15:07:13 +02:00
|
|
|
rule_start_re = /(?:#{COMMENT_RE}*)^#{rule_re}/
|
2013-08-01 01:58:26 +02:00
|
|
|
else
|
2013-08-03 00:22:57 +02:00
|
|
|
rule_start_re = /^#{rule_re}/
|
2013-08-01 01:58:26 +02:00
|
|
|
end
|
2013-08-03 00:22:57 +02:00
|
|
|
|
2013-08-20 15:07:13 +02:00
|
|
|
positions = []
|
2013-08-20 20:09:56 +02:00
|
|
|
while (rule_start = s.scan_next(rule_start_re))
|
|
|
|
pos = s.pos
|
2013-08-20 15:07:13 +02:00
|
|
|
positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
|
2013-07-31 02:27:29 +02:00
|
|
|
end
|
2013-08-20 15:07:13 +02:00
|
|
|
replace_substrings_at(less, positions, &block)
|
2013-07-31 02:27:29 +02:00
|
|
|
less
|
|
|
|
end
|
|
|
|
|
2013-08-20 21:12:25 +02:00
|
|
|
# Get a all top-level selectors (with {)
|
|
|
|
def get_css_selectors(css, opts = {})
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(css)
|
2013-08-18 23:00:27 +02:00
|
|
|
selectors = []
|
2013-08-20 21:12:25 +02:00
|
|
|
while s.scan_next(RULE_OPEN_BRACE_RE)
|
|
|
|
brace_pos = s.pos
|
|
|
|
def_pos = css_def_pos(css, brace_pos+1, -1)
|
|
|
|
sel = css[def_pos.begin..brace_pos - 1].dup
|
|
|
|
sel.strip! if opts[:strip]
|
|
|
|
selectors << sel
|
|
|
|
sel.dup.strip
|
|
|
|
s.pos = close_brace_pos(css, brace_pos, 1) + 1
|
2013-08-18 23:00:27 +02:00
|
|
|
end
|
|
|
|
selectors
|
|
|
|
end
|
|
|
|
|
2013-07-31 17:04:30 +02:00
|
|
|
# replace in the top-level selector
|
|
|
|
# replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
|
|
|
|
def replace_in_selector(css, pattern, sub)
|
|
|
|
# scan for selector positions in css
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(css)
|
2013-07-31 17:04:30 +02:00
|
|
|
prev_pos = 0
|
2013-08-03 00:22:57 +02:00
|
|
|
sel_pos = []
|
2013-08-20 20:09:56 +02:00
|
|
|
while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
|
|
|
|
pos = s.pos
|
2013-08-18 23:00:27 +02:00
|
|
|
sel_pos << (prev_pos .. pos - 1)
|
2013-08-20 20:09:56 +02:00
|
|
|
s.pos = close_brace_pos(css, s.pos - 1) + 1
|
2013-08-18 23:00:27 +02:00
|
|
|
prev_pos = pos
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
2013-08-03 00:22:57 +02:00
|
|
|
replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-08-03 00:22:57 +02:00
|
|
|
sel_chars = '\[\]$\w\-{}#,.:&>@'
|
|
|
|
SELECTOR_RE = /[#{sel_chars}]+[#{sel_chars}\s]*/
|
|
|
|
COMMENT_RE = %r((?:^[ \t]*//[^\n]*\n))
|
2013-08-18 23:00:27 +02:00
|
|
|
RULE_OPEN_BRACE_RE = /(?<![@#\$])\{/
|
|
|
|
RULE_OPEN_BRACE_RE_REVERSE = /\{(?![@#\$])/
|
|
|
|
RULE_CLOSE_BRACE_RE = /(?<!\w)\}(?![.'"])/
|
|
|
|
RULE_CLOSE_BRACE_RE_REVERSE = /(?<![.'"])\}(?!\w)/
|
2013-08-14 01:42:45 +02:00
|
|
|
BRACE_RE = /#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m
|
|
|
|
BRACE_RE_REVERSE = /#{RULE_OPEN_BRACE_RE_REVERSE}|#{RULE_CLOSE_BRACE_RE_REVERSE}/m
|
2013-09-29 00:05:08 +02:00
|
|
|
SCSS_MIXIN_DEF_ARGS_RE = /[\w\-,\s$:#%()]*/
|
|
|
|
LESS_MIXIN_DEF_ARGS_RE = /[\w\-,;.\s@:#%()]*/
|
2013-07-31 17:04:30 +02:00
|
|
|
|
|
|
|
# replace first level properties in the css with yields
|
|
|
|
# replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
|
|
|
|
def replace_properties(css, &block)
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(css)
|
2013-08-03 00:22:57 +02:00
|
|
|
s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
|
2013-07-31 17:04:30 +02:00
|
|
|
prev_pos = s.pos
|
|
|
|
depth = 0
|
|
|
|
pos = []
|
2013-08-20 20:09:56 +02:00
|
|
|
while (b = s.scan_next(/#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m))
|
|
|
|
s_pos = s.pos
|
2013-08-03 00:22:57 +02:00
|
|
|
depth += (b == '}' ? -1 : +1)
|
|
|
|
if depth == 1
|
2013-07-31 17:04:30 +02:00
|
|
|
if b == '}'
|
2013-08-18 23:00:27 +02:00
|
|
|
prev_pos = s_pos
|
2013-07-31 17:04:30 +02:00
|
|
|
else
|
2013-08-18 23:00:27 +02:00
|
|
|
pos << (prev_pos .. s_pos - b.length - 1)
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2013-08-03 00:22:57 +02:00
|
|
|
replace_substrings_at css, pos, &block
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-08-14 17:38:43 +02:00
|
|
|
# immediate selector of css at pos
|
|
|
|
def selector_for_pos(css, pos, depth = -1)
|
2013-08-18 23:00:27 +02:00
|
|
|
css[css_def_pos(css, pos, depth)].dup.strip
|
2013-08-14 17:38:43 +02:00
|
|
|
end
|
|
|
|
|
2013-08-14 01:42:45 +02:00
|
|
|
# get the pos of css def at pos (search backwards)
|
|
|
|
def css_def_pos(css, pos, depth = -1)
|
|
|
|
to = open_brace_pos(css, pos, depth)
|
2013-08-14 17:38:43 +02:00
|
|
|
prev_def = to - (css[0..to].reverse.index('}') || to) + 1
|
2013-08-14 01:42:45 +02:00
|
|
|
from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
|
|
|
|
(from..to - 1)
|
|
|
|
end
|
|
|
|
|
|
|
|
# next matching brace for brace at from
|
|
|
|
def close_brace_pos(css, from, depth = 0)
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(css[from..-1])
|
|
|
|
while (b = s.scan_next(BRACE_RE))
|
2013-07-31 02:27:29 +02:00
|
|
|
depth += (b == '}' ? -1 : +1)
|
|
|
|
break if depth.zero?
|
|
|
|
end
|
|
|
|
raise "match not found for {" unless depth.zero?
|
2013-08-20 20:09:56 +02:00
|
|
|
from + s.pos - 1
|
2013-08-14 01:42:45 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
# opening brace position from +from+ (search backwards)
|
|
|
|
def open_brace_pos(css, from, depth = 0)
|
2013-08-20 20:09:56 +02:00
|
|
|
s = CharStringScanner.new(css[0..from].reverse)
|
|
|
|
while (b = s.scan_next(BRACE_RE_REVERSE))
|
2013-08-14 01:42:45 +02:00
|
|
|
depth += (b == '{' ? +1 : -1)
|
|
|
|
break if depth.zero?
|
|
|
|
end
|
|
|
|
raise "matching { brace not found" unless depth.zero?
|
2013-08-20 20:09:56 +02:00
|
|
|
from - s.pos + 1
|
2013-07-31 02:27:29 +02:00
|
|
|
end
|
2013-07-31 17:04:30 +02:00
|
|
|
|
2013-08-03 00:22:57 +02:00
|
|
|
# insert substitutions into text at positions (Range or Fixnum)
|
|
|
|
# substitutions can be passed as array or as yields from the &block called with |substring, position, text|
|
|
|
|
# position is a range (begin..end)
|
|
|
|
def replace_substrings_at(text, positions, replacements = nil, &block)
|
2013-07-31 17:04:30 +02:00
|
|
|
offset = 0
|
2013-08-03 00:22:57 +02:00
|
|
|
positions.each_with_index do |p, i|
|
|
|
|
p = (p...p) if p.is_a?(Fixnum)
|
2013-08-20 15:07:13 +02:00
|
|
|
from = p.begin + offset
|
|
|
|
to = p.end + offset
|
2013-08-14 01:42:45 +02:00
|
|
|
p = p.exclude_end? ? (from...to) : (from..to)
|
2013-08-03 00:22:57 +02:00
|
|
|
# block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
|
|
|
|
r = replacements ? replacements[i] : block.call(text[p], p, text)
|
|
|
|
text[p] = r
|
2013-08-14 01:42:45 +02:00
|
|
|
# add the change in length to offset
|
|
|
|
offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
2013-08-03 00:22:57 +02:00
|
|
|
text
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
2013-08-03 09:31:57 +02:00
|
|
|
|
|
|
|
def get_json(url)
|
2013-08-05 02:22:14 +02:00
|
|
|
JSON.parse get_file(url)
|
|
|
|
end
|
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
# regular string scanner works with bytes
|
|
|
|
# this one works with chars and provides #scan_next
|
|
|
|
class CharStringScanner
|
|
|
|
extend Forwardable
|
|
|
|
|
|
|
|
def initialize(*args)
|
|
|
|
@s = StringScanner.new(*args)
|
|
|
|
end
|
|
|
|
|
|
|
|
def_delegators :@s, :scan_until, :skip_until, :string
|
|
|
|
|
|
|
|
# advance scanner to pos after the next match of pattern and return the match
|
|
|
|
def scan_next(pattern)
|
|
|
|
return unless @s.scan_until(pattern)
|
|
|
|
@s.matched
|
|
|
|
end
|
|
|
|
|
|
|
|
def pos
|
|
|
|
byte_to_str_pos @s.pos
|
|
|
|
end
|
|
|
|
|
|
|
|
def pos=(i)
|
2013-08-20 21:12:25 +02:00
|
|
|
@s.pos = str_to_byte_pos i
|
2013-08-20 20:09:56 +02:00
|
|
|
i
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def byte_to_str_pos(pos)
|
|
|
|
@s.string.byteslice(0, pos).length
|
|
|
|
end
|
|
|
|
|
|
|
|
def str_to_byte_pos(pos)
|
|
|
|
@s.string.slice(0, pos).bytesize
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-04 23:22:54 +02:00
|
|
|
class Logger
|
|
|
|
include Term::ANSIColor
|
|
|
|
|
|
|
|
def initialize(env)
|
|
|
|
@env = env
|
|
|
|
puts bold "Convert Bootstrap LESS to SASS"
|
2013-08-05 01:46:33 +02:00
|
|
|
puts " repo : #{env[:repo]}"
|
|
|
|
puts " branch : #{env[:branch]} #{dark "#{env[:repo]}/tree/#{env[:branch_sha]}"}"
|
2013-08-20 18:30:06 +02:00
|
|
|
puts " save to: #{@env[:save_at].to_json}"
|
2013-08-18 20:13:13 +02:00
|
|
|
puts " twbs cache: #{@env[:cache_path]}"
|
2013-08-05 01:36:20 +02:00
|
|
|
puts dark "-" * 60
|
2013-08-04 23:22:54 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def log_status(status)
|
2013-08-05 01:36:20 +02:00
|
|
|
puts bold status
|
2013-08-04 23:22:54 +02:00
|
|
|
end
|
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def log_file_info(s)
|
|
|
|
puts " #{magenta s}"
|
2013-08-04 23:37:34 +02:00
|
|
|
end
|
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def log_transform(*args)
|
|
|
|
puts "#{cyan " #{caller[1][/`.*'/][1..-2].sub(/^block in /, '')}"}#{cyan ": #{args * ', '}" unless args.empty?}"
|
2013-08-05 00:21:17 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
def log_processing(name)
|
|
|
|
puts yellow " #{File.basename(name)}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def log_processed(name)
|
2013-08-05 00:44:05 +02:00
|
|
|
puts green " #{name}"
|
2013-08-04 23:22:54 +02:00
|
|
|
end
|
2013-08-05 02:22:14 +02:00
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def log_http_get_file(url, cached = false)
|
2013-08-20 15:07:13 +02:00
|
|
|
s = " #{'CACHED ' if cached}GET #{url}..."
|
|
|
|
if cached
|
|
|
|
puts dark green s
|
|
|
|
else
|
|
|
|
puts dark cyan s
|
|
|
|
end
|
2013-08-05 02:22:14 +02:00
|
|
|
end
|
2013-08-14 01:42:45 +02:00
|
|
|
|
2013-08-20 20:09:56 +02:00
|
|
|
def log_http_get_files(files, from, cached = false)
|
|
|
|
s = " #{'CACHED ' if cached}GET #{files.length} files from #{from} #{files * ' '}..."
|
|
|
|
if cached
|
|
|
|
puts dark green s
|
|
|
|
else
|
|
|
|
puts dark cyan s
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-08-14 01:42:45 +02:00
|
|
|
def puts(*args)
|
|
|
|
STDOUT.puts *args unless @silence
|
|
|
|
end
|
|
|
|
|
|
|
|
def silence_log
|
|
|
|
@silence = true
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
@silence = false
|
|
|
|
end
|
2013-08-04 23:22:54 +02:00
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|