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-05-23 14:51:56 -07:00
|
|
|
|
|
|
|
class Converter
|
2013-06-13 13:58:34 -07:00
|
|
|
def initialize(branch)
|
2013-07-30 16:05:48 +02:00
|
|
|
@repo = 'twbs/bootstrap'
|
2013-06-13 13:58:34 -07:00
|
|
|
@branch = branch || 'master'
|
2013-07-28 18:14:12 +02:00
|
|
|
@mixins = get_mixins_name
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def process
|
2013-05-24 16:59:48 -07:00
|
|
|
process_stylesheet_assets
|
|
|
|
process_javascript_assets
|
|
|
|
end
|
|
|
|
|
|
|
|
def process_stylesheet_assets
|
|
|
|
puts "\nProcessing stylesheets..."
|
2013-07-30 22:08:49 +02:00
|
|
|
read_files('less', bootstrap_less_files).each do |name, file|
|
2013-06-15 16:30:44 -07:00
|
|
|
case name
|
|
|
|
when 'bootstrap.less'
|
|
|
|
file = replace_file_imports(file)
|
|
|
|
when 'mixins.less'
|
|
|
|
file = replace_vars(file)
|
|
|
|
file = replace_escaping(file)
|
|
|
|
file = replace_mixin_file(file)
|
|
|
|
file = replace_mixins(file)
|
2013-07-31 18:45:17 +02:00
|
|
|
file = flatten_mixins(file, '#gradient', 'gradient')
|
2013-07-31 17:04:30 +02:00
|
|
|
file = parameterize_mixin_parent_selector(file, 'responsive-(in)?visibility')
|
2013-07-31 18:02:23 +02:00
|
|
|
when 'responsive-utilities.less'
|
|
|
|
file = convert_to_scss(file)
|
2013-07-31 18:06:22 +02:00
|
|
|
file = apply_mixin_parent_selector(file, '\.(visible|hidden)')
|
2013-06-15 16:30:44 -07:00
|
|
|
when 'utilities.less'
|
|
|
|
file = replace_mixin_file(file)
|
|
|
|
file = convert_to_scss(file)
|
|
|
|
when 'variables.less'
|
|
|
|
file = convert_to_scss(file)
|
|
|
|
file = insert_default_vars(file)
|
2013-07-31 18:45:17 +02:00
|
|
|
when 'close.less'
|
|
|
|
file = convert_to_scss(file)
|
|
|
|
# extract .close { button& {...} } rule
|
2013-07-31 19:10:50 +02:00
|
|
|
file = extract_nested_rule(file, '\s*button&', 'button.close')
|
2013-06-15 16:30:44 -07:00
|
|
|
else
|
|
|
|
file = convert_to_scss(file)
|
|
|
|
end
|
|
|
|
|
|
|
|
name = name.gsub(/\.less$/, '.scss')
|
|
|
|
if name == 'bootstrap.scss'
|
|
|
|
path = "vendor/assets/stylesheets/bootstrap/bootstrap.scss"
|
|
|
|
else
|
2013-05-24 16:59:48 -07:00
|
|
|
path = "vendor/assets/stylesheets/bootstrap/_#{name}"
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
2013-06-15 16:30:44 -07:00
|
|
|
save_file(path, file)
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def process_javascript_assets
|
|
|
|
puts "\nProcessing javascripts..."
|
2013-07-30 22:08:49 +02:00
|
|
|
read_files('js', bootstrap_js_files).each do |name, file|
|
|
|
|
save_file("vendor/assets/javascripts/bootstrap/#{name}", file)
|
2013-05-24 16:59:48 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
# Update javascript manifest
|
|
|
|
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)
|
|
|
|
end
|
|
|
|
|
2013-05-23 14:51:56 -07:00
|
|
|
private
|
|
|
|
|
2013-07-30 22:08:49 +02:00
|
|
|
def read_files(path, files)
|
|
|
|
contents = {}
|
|
|
|
files.map do |name|
|
|
|
|
url = "https://raw.github.com/#@repo/#@branch/#{path}/#{name}"
|
|
|
|
Thread.start {
|
|
|
|
content = open(url).read
|
|
|
|
Thread.exclusive {
|
|
|
|
puts "GET #{url}"
|
|
|
|
contents[name] = content
|
|
|
|
}
|
|
|
|
}
|
|
|
|
end.each(&:join)
|
|
|
|
contents
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
# Get the sha of a dir
|
|
|
|
def get_tree_sha(dir)
|
2013-07-30 16:05:48 +02:00
|
|
|
trees = open("https://api.github.com/repos/#@repo/git/trees/#@branch").read
|
2013-05-23 14:51:56 -07:00
|
|
|
trees = JSON.parse trees
|
2013-05-24 16:59:48 -07:00
|
|
|
trees['tree'].find{|t| t['path'] == dir}['sha']
|
|
|
|
end
|
|
|
|
|
|
|
|
def bootstrap_less_files
|
2013-07-30 22:08:49 +02:00
|
|
|
@bootstrap_less_files ||= begin
|
|
|
|
files = open("https://api.github.com/repos/#@repo/git/trees/#{get_tree_sha('less')}").read
|
|
|
|
files = JSON.parse files
|
|
|
|
files['tree'].select{|f| f['type'] == 'blob' && f['path'] =~ /.less$/ }.map{|f| f['path'] }
|
|
|
|
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
|
|
|
|
files = open("https://api.github.com/repos/#@repo/git/trees/#{get_tree_sha('js')}").read
|
|
|
|
files = JSON.parse files
|
|
|
|
files = files['tree'].select { |f| f['type'] == 'blob' && f['path'] =~ /.js$/ }.map { |f| f['path'] }
|
|
|
|
files.sort_by { |f|
|
|
|
|
case f
|
|
|
|
# tooltip depends on popover and must be loaded earlier
|
|
|
|
when /tooltip/ then
|
|
|
|
1
|
|
|
|
when /popover/ then
|
|
|
|
2
|
|
|
|
else
|
|
|
|
0
|
|
|
|
end
|
|
|
|
}
|
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def get_mixins_name
|
|
|
|
mixins = []
|
2013-07-30 16:05:48 +02:00
|
|
|
less_mixins = open("https://raw.github.com/#@repo/#@branch/less/mixins.less").read
|
2013-05-23 14:51:56 -07:00
|
|
|
|
|
|
|
less_mixins.scan(/\.([\w-]+)\(.*\)\s?{?/) do |mixin|
|
2013-07-28 18:14:12 +02:00
|
|
|
mixins << mixin.first
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
mixins
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def convert_to_scss(file)
|
2013-05-23 14:51:56 -07:00
|
|
|
file = replace_vars(file)
|
|
|
|
file = replace_mixins(file)
|
|
|
|
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)
|
|
|
|
file
|
|
|
|
end
|
|
|
|
|
2013-05-24 16:59:48 -07:00
|
|
|
def save_file(path, content, mode='w')
|
|
|
|
File.open(path, mode) { |file| file.write(content) }
|
|
|
|
puts "Saved #{path}\n"
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
2013-06-15 16:30:44 -07:00
|
|
|
def replace_file_imports(less)
|
|
|
|
less.gsub(/@import ["|']([\w-]+).less["|'];/, '@import "bootstrap/\1";');
|
|
|
|
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)
|
|
|
|
param = '$parent'
|
|
|
|
replace_rules(file, '\s*@mixin\s*' + rule_sel) do |mxn_css|
|
|
|
|
mxn_css.sub! /(@mixin [\w-]+\()/, "\\1#{param}"
|
|
|
|
replace_properties(mxn_css) { |props| " \#{#{param}} { #{props.strip} }\n" }
|
2013-07-31 18:45:17 +02:00
|
|
|
replace_rules(mxn_css) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
|
2013-07-31 17:04:30 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-07-31 19:10:50 +02:00
|
|
|
# extracts rule immediately after it's parent and optionally changes selector to new_selector
|
|
|
|
def extract_nested_rule(css, selector, new_selector = selector)
|
|
|
|
rule = pos = nil
|
|
|
|
css = replace_rules(css, selector) { |r, p| rule = r; pos = p; '' }
|
|
|
|
# replace rule selector with new_selector
|
|
|
|
rule = rule.sub /^(\s*).*?(\s*){/m, "\\1#{new_selector}\\2{"
|
|
|
|
css.insert pos.begin + css[pos.begin..-1].index('}') + 1,
|
|
|
|
"\n" + unindent(rule)
|
|
|
|
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)
|
|
|
|
replace_rules file, "(\s*)#{rule_sel}" do |rule|
|
|
|
|
next rule unless rule =~ /@include/
|
|
|
|
rule =~ /\A\s+/ # keep indentation
|
|
|
|
$~.to_s + rule.sub(/(#{SELECTOR_RE}){(.*)}/m, '\2').sub(/(@include [\w-]+\()/, "\\1'#{$1.strip}'").strip
|
|
|
|
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-07-31 16:02:19 +02:00
|
|
|
replace_rules file, Regexp.escape(container) do |mixins_css|
|
2013-07-31 02:27:29 +02:00
|
|
|
unwrapped = mixins_css.split("\n")[1..-2] * "\n"
|
2013-07-31 18:45:17 +02:00
|
|
|
unindent(unwrapped.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-07-16 23:29:41 -07:00
|
|
|
# .mixin() -> @import mixin()
|
|
|
|
# #scope > .mixin() -> @import scope-mixin()
|
2013-05-23 14:51:56 -07:00
|
|
|
def replace_mixins(less)
|
2013-07-28 18:01:43 +02:00
|
|
|
mixin_pattern = /(\s+)(([#|\.][\w-]+\s*>\s*)*)\.([\w-]+\(.*\))/
|
2013-05-28 20:50:03 -07:00
|
|
|
less.gsub(mixin_pattern) do |match|
|
|
|
|
matches = match.scan(mixin_pattern).flatten
|
|
|
|
scope = matches[1] || ''
|
|
|
|
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-07-28 18:14:12 +02:00
|
|
|
mixin_name = match.scan(/\.([\w-]+)\(.*\)\s?{?/).first
|
|
|
|
|
|
|
|
if mixin_name && @mixins.include?(mixin_name.first)
|
|
|
|
"#{matches.first}@include #{scope}#{matches.last}".gsub(/; \$/,', $')
|
|
|
|
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
|
|
|
|
|
|
|
|
def replace_mixin_file(less)
|
2013-07-30 18:02:33 +02:00
|
|
|
less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
|
|
|
|
"#{$1}@mixin #{$2.tr(';', ',')}#{$3}"
|
|
|
|
}
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def replace_vars(less)
|
2013-07-30 17:07:44 +02:00
|
|
|
less = less.gsub(/(?!@media|@page|@keyframes|@font-face|@-\w)@/, '$')
|
|
|
|
# variables that would be ignored by gsub above: e.g. @page-header-border-color
|
2013-07-30 17:10:41 +02:00
|
|
|
less.gsub! /@(page[\w-]+)/, '$\1'
|
2013-07-30 17:07:44 +02:00
|
|
|
less
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def replace_less_extend(less)
|
|
|
|
less.gsub(/\#(\w+) \> \.([\w-]*)(\(.*\));?/, '@include \1-\2\3;')
|
|
|
|
end
|
|
|
|
|
|
|
|
def replace_spin(less)
|
|
|
|
less.gsub(/spin/, 'adjust-hue')
|
|
|
|
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-07-28 18:21:05 +02:00
|
|
|
less.gsub!(/\${([^}]+)}/, '$\1') # Get rid of @{} escape
|
|
|
|
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-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)
|
|
|
|
txt.gsub /^\s{1,#{n}}/, ''
|
|
|
|
end
|
|
|
|
|
|
|
|
# replace CSS rule blocks matching rule_prefix with yields
|
|
|
|
#
|
|
|
|
# replace_rules(".a{ \n .b{} }", '.b') { |rule| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
|
|
|
|
def replace_rules(less, rule_prefix = '\s*')
|
|
|
|
less = less.dup
|
|
|
|
s = StringScanner.new(less)
|
|
|
|
rule_start_re = /^#{rule_prefix}[^{]*{/
|
|
|
|
while (rule_start = scan_next(s, rule_start_re))
|
|
|
|
rule_pos = (s.pos - rule_start.length..next_brace_pos(less, s.pos - 1))
|
|
|
|
group = less[rule_pos]
|
2013-07-31 19:10:50 +02:00
|
|
|
less[rule_pos] = yield(group, rule_pos)
|
2013-07-31 02:27:29 +02:00
|
|
|
end
|
|
|
|
less
|
|
|
|
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
|
|
|
|
s = StringScanner.new(css)
|
|
|
|
prev_pos = 0
|
|
|
|
sel_pos = []
|
|
|
|
while (brace = scan_next(s, /\{/))
|
|
|
|
sel_pos << (prev_pos .. s.pos - 1)
|
|
|
|
s.pos = next_brace_pos(css, s.pos - 1) + 1
|
|
|
|
prev_pos = s.pos
|
|
|
|
end
|
|
|
|
# insert replacements
|
|
|
|
insert_sub(css, sel_pos) { |css, p| css[p].gsub(pattern, sub) }
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-07-31 18:02:23 +02:00
|
|
|
SELECTOR_RE = /[$\w\-{}#\s,.:&]+/
|
2013-07-31 17:04:30 +02:00
|
|
|
BRACE_RE = /(?![#])[{}]/m
|
|
|
|
|
|
|
|
# replace first level properties in the css with yields
|
|
|
|
# replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
|
|
|
|
def replace_properties(css, &block)
|
|
|
|
s = StringScanner.new(css)
|
|
|
|
s.skip_until /{\n?/
|
|
|
|
prev_pos = s.pos
|
|
|
|
depth = 0
|
|
|
|
pos = []
|
|
|
|
while (b = scan_next(s, /#{SELECTOR_RE}(?![#])\{\n?|\}/))
|
|
|
|
if depth.zero?
|
|
|
|
if b == '}'
|
|
|
|
prev_pos = s.pos
|
|
|
|
else
|
|
|
|
pos << (prev_pos .. s.pos - b.length )
|
|
|
|
end
|
|
|
|
depth += (b == '}' ? -1 : +1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
insert_sub(css, pos) { |css, p| yield(css[p]) }
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2013-07-31 02:27:29 +02:00
|
|
|
# next matching brace for brace at brace_pos in css
|
|
|
|
def next_brace_pos(css, brace_pos)
|
|
|
|
depth = 0
|
|
|
|
s = StringScanner.new(css[brace_pos..-1])
|
2013-07-31 17:04:30 +02:00
|
|
|
while (b = scan_next(s, 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?
|
|
|
|
brace_pos + s.pos - 1
|
|
|
|
end
|
|
|
|
|
2013-07-31 16:02:19 +02:00
|
|
|
# advance scanner to pos after the next match of pattern and return the match
|
2013-07-31 02:27:29 +02:00
|
|
|
def scan_next(scanner, pattern)
|
|
|
|
return unless scanner.skip_until(pattern)
|
|
|
|
scanner.pos -= scanner.matched_size
|
|
|
|
scanner.scan pattern
|
|
|
|
end
|
2013-07-31 17:04:30 +02:00
|
|
|
|
|
|
|
# insert substitutions into css at positions
|
|
|
|
# substitutions are yields from block called with (css, (begin..end))
|
|
|
|
def insert_sub(css, positions, &block)
|
|
|
|
offset = 0
|
|
|
|
positions.each do |p|
|
|
|
|
p = (p.begin + offset .. p.end + offset)
|
|
|
|
r = block.call(css, p)
|
2013-07-31 21:53:39 +02:00
|
|
|
offset += r.size - (p.end - p.begin + 1)
|
2013-07-31 17:04:30 +02:00
|
|
|
css[p] = r
|
|
|
|
end
|
|
|
|
css
|
|
|
|
end
|
2013-05-23 14:51:56 -07:00
|
|
|
end
|