entreprise7pro-bootstrap-sass 3.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +233 -0
- data/CONTRIBUTING.md +86 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +376 -0
- data/Rakefile +98 -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/affix.js +166 -0
- data/assets/javascripts/bootstrap/alert.js +97 -0
- data/assets/javascripts/bootstrap/button.js +136 -0
- data/assets/javascripts/bootstrap/carousel.js +248 -0
- data/assets/javascripts/bootstrap/collapse.js +214 -0
- data/assets/javascripts/bootstrap/dropdown.js +167 -0
- data/assets/javascripts/bootstrap/modal.js +356 -0
- data/assets/javascripts/bootstrap/popover.js +123 -0
- data/assets/javascripts/bootstrap/scrollspy.js +174 -0
- data/assets/javascripts/bootstrap/tab.js +157 -0
- data/assets/javascripts/bootstrap/tooltip.js +679 -0
- data/assets/javascripts/bootstrap/transition.js +61 -0
- data/assets/javascripts/bootstrap-sprockets.js +12 -0
- data/assets/javascripts/bootstrap.js +2611 -0
- data/assets/javascripts/bootstrap.min.js +7 -0
- data/assets/javascripts/jquery.min.js +2 -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/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 +263 -0
- data/assets/stylesheets/bootstrap/_close.scss +37 -0
- data/assets/stylesheets/bootstrap/_code.scss +70 -0
- data/assets/stylesheets/bootstrap/_component-animations.scss +38 -0
- data/assets/stylesheets/bootstrap/_dropdowns.scss +212 -0
- data/assets/stylesheets/bootstrap/_forms.scss +602 -0
- data/assets/stylesheets/bootstrap/_glyphicons.scss +307 -0
- data/assets/stylesheets/bootstrap/_grid.scss +94 -0
- data/assets/stylesheets/bootstrap/_input-groups.scss +166 -0
- data/assets/stylesheets/bootstrap/_jumbotron.scss +55 -0
- data/assets/stylesheets/bootstrap/_labels.scss +66 -0
- data/assets/stylesheets/bootstrap/_list-group.scss +128 -0
- data/assets/stylesheets/bootstrap/_media.scss +66 -0
- data/assets/stylesheets/bootstrap/_mixins.scss +39 -0
- data/assets/stylesheets/bootstrap/_modals.scss +150 -0
- data/assets/stylesheets/bootstrap/_navbar.scss +657 -0
- data/assets/stylesheets/bootstrap/_navs.scss +242 -0
- data/assets/stylesheets/bootstrap/_normalize.scss +422 -0
- data/assets/stylesheets/bootstrap/_pager.scss +54 -0
- data/assets/stylesheets/bootstrap/_pagination.scss +86 -0
- data/assets/stylesheets/bootstrap/_panels.scss +271 -0
- data/assets/stylesheets/bootstrap/_popovers.scss +126 -0
- data/assets/stylesheets/bootstrap/_print.scss +99 -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 +157 -0
- data/assets/stylesheets/bootstrap/_scaffolding.scss +161 -0
- data/assets/stylesheets/bootstrap/_tables.scss +233 -0
- data/assets/stylesheets/bootstrap/_theme.scss +293 -0
- data/assets/stylesheets/bootstrap/_thumbnails.scss +38 -0
- data/assets/stylesheets/bootstrap/_tooltip.scss +112 -0
- data/assets/stylesheets/bootstrap/_type.scss +299 -0
- data/assets/stylesheets/bootstrap/_utilities.scss +55 -0
- data/assets/stylesheets/bootstrap/_variables.scss +875 -0
- data/assets/stylesheets/bootstrap/_wells.scss +29 -0
- data/assets/stylesheets/bootstrap/mixins/_alerts.scss +15 -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 +61 -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 +50 -0
- data/assets/stylesheets/bootstrap/mixins/_grid-framework.scss +82 -0
- data/assets/stylesheets/bootstrap/mixins/_grid.scss +123 -0
- data/assets/stylesheets/bootstrap/mixins/_hide-text.scss +21 -0
- data/assets/stylesheets/bootstrap/mixins/_image.scss +28 -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 +11 -0
- data/assets/stylesheets/bootstrap/mixins/_nav-vertical-align.scss +10 -0
- data/assets/stylesheets/bootstrap/mixins/_opacity.scss +7 -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-text.scss +18 -0
- data/assets/stylesheets/bootstrap/mixins/_resize.scss +6 -0
- data/assets/stylesheets/bootstrap/mixins/_responsive-visibility.scss +17 -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 +210 -0
- data/bower.json +38 -0
- data/composer.json +21 -0
- data/entreprise7pro-bootstrap-sass.gemspec +37 -0
- data/eyeglass-exports.js +7 -0
- data/lib/entreprise7pro-bootstrap-sass/engine.rb +17 -0
- data/lib/entreprise7pro-bootstrap-sass/version.rb +4 -0
- data/lib/entreprise7pro-bootstrap-sass.rb +91 -0
- data/package-lock.json +1011 -0
- data/package.json +48 -0
- data/sache.json +5 -0
- data/tasks/bower.rake +31 -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 +752 -0
- data/tasks/converter/logger.rb +57 -0
- data/tasks/converter/network.rb +97 -0
- data/tasks/converter.rb +80 -0
- data/templates/project/_bootstrap-variables.sass +876 -0
- data/templates/project/manifest.rb +20 -0
- data/templates/project/styles.sass +6 -0
- data/test/compilation_test.rb +30 -0
- data/test/dummy_node_mincer/apple-touch-icon-144-precomposed.png +0 -0
- data/test/dummy_node_mincer/application.css.ejs.scss +6 -0
- data/test/dummy_node_mincer/manifest.js +87 -0
- data/test/dummy_rails/README.rdoc +3 -0
- data/test/dummy_rails/Rakefile +6 -0
- data/test/dummy_rails/app/assets/images/.keep +0 -0
- data/test/dummy_rails/app/assets/javascripts/application.js +2 -0
- data/test/dummy_rails/app/assets/stylesheets/application.sass +2 -0
- data/test/dummy_rails/app/controllers/application_controller.rb +5 -0
- data/test/dummy_rails/app/controllers/pages_controller.rb +4 -0
- data/test/dummy_rails/app/helpers/application_helper.rb +2 -0
- data/test/dummy_rails/app/views/layouts/application.html.erb +14 -0
- data/test/dummy_rails/app/views/pages/root.html.slim +84 -0
- data/test/dummy_rails/config/application.rb +31 -0
- data/test/dummy_rails/config/boot.rb +5 -0
- data/test/dummy_rails/config/environment.rb +5 -0
- data/test/dummy_rails/config/environments/development.rb +23 -0
- data/test/dummy_rails/config/environments/production.rb +82 -0
- data/test/dummy_rails/config/environments/test.rb +38 -0
- data/test/dummy_rails/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy_rails/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy_rails/config/initializers/inflections.rb +16 -0
- data/test/dummy_rails/config/initializers/mime_types.rb +5 -0
- data/test/dummy_rails/config/initializers/secret_token.rb +18 -0
- data/test/dummy_rails/config/initializers/session_store.rb +3 -0
- data/test/dummy_rails/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy_rails/config/locales/en.yml +3 -0
- data/test/dummy_rails/config/locales/es.yml +3 -0
- data/test/dummy_rails/config/routes.rb +3 -0
- data/test/dummy_rails/config.ru +4 -0
- data/test/dummy_rails/log/.keep +0 -0
- data/test/dummy_sass_only/Gemfile +4 -0
- data/test/dummy_sass_only/compile.rb +20 -0
- data/test/dummy_sass_only/import_all.scss +2 -0
- data/test/gemfiles/default.gemfile +3 -0
- data/test/node_mincer_test.rb +35 -0
- data/test/node_sass_compile_test.sh +9 -0
- data/test/pages_test.rb +14 -0
- data/test/sass_test.rb +29 -0
- data/test/sprockets_rails_test.rb +31 -0
- data/test/support/dummy_rails_integration.rb +22 -0
- data/test/support/reporting.rb +27 -0
- data/test/test_helper.rb +36 -0
- data/test/test_helper_rails.rb +6 -0
- metadata +467 -0
@@ -0,0 +1,752 @@
|
|
1
|
+
require_relative 'char_string_scanner'
|
2
|
+
require 'bootstrap-sass/version'
|
3
|
+
|
4
|
+
# This is the script used to automatically convert all of entreprise7pro/bootstrap LESS to Sass.
|
5
|
+
#
|
6
|
+
# Most differences are fixed by regexps and other forms of string substitution.
|
7
|
+
# There are Bootstrap-specific workarounds for the lack of parent selectors, recursion, mixin namespaces, extend within @media, etc in Sass 3.2.
|
8
|
+
class Converter
|
9
|
+
module LessConversion
|
10
|
+
# Some regexps for matching bits of SCSS:
|
11
|
+
SELECTOR_CHAR = '\[\]$\w\-{}#,.:&>@'
|
12
|
+
# 1 selector (the part before the {)
|
13
|
+
SELECTOR_RE = /[#{SELECTOR_CHAR}]+[#{SELECTOR_CHAR}\s]*/
|
14
|
+
# 1 // comment
|
15
|
+
COMMENT_RE = %r((?:^[ \t]*//[^\n]*\n))
|
16
|
+
# 1 {, except when part of @{ and #{
|
17
|
+
RULE_OPEN_BRACE_RE = /(?<![@#\$])\{/
|
18
|
+
# same as the one above, but in reverse (on a reversed string)
|
19
|
+
RULE_OPEN_BRACE_RE_REVERSE = /\{(?![@#\$])/
|
20
|
+
# match closed brace, except when \w precedes }, or when }[.'"]. a heurestic to exclude } that are not selector body close }
|
21
|
+
RULE_CLOSE_BRACE_RE = /(?<!\w)\}(?![.'"])/
|
22
|
+
RULE_CLOSE_BRACE_RE_REVERSE = /(?<![.'"])\}(?!\w)/
|
23
|
+
# match any brace that opens or closes a properties body
|
24
|
+
BRACE_RE = /#{RULE_OPEN_BRACE_RE}|#{RULE_CLOSE_BRACE_RE}/m
|
25
|
+
BRACE_RE_REVERSE = /#{RULE_OPEN_BRACE_RE_REVERSE}|#{RULE_CLOSE_BRACE_RE_REVERSE}/m
|
26
|
+
# valid characters in mixin definitions
|
27
|
+
SCSS_MIXIN_DEF_ARGS_RE = /[\w\-,\s$:#%()]*/
|
28
|
+
LESS_MIXIN_DEF_ARGS_RE = /[\w\-,;.\s@:#%()]*/
|
29
|
+
|
30
|
+
# These mixins will get vararg definitions in SCSS (not supported by LESS):
|
31
|
+
NESTED_MIXINS = {'#gradient' => 'gradient'}
|
32
|
+
|
33
|
+
# These mixins will get vararg definitions in SCSS (not supported by LESS):
|
34
|
+
VARARG_MIXINS = %w(
|
35
|
+
scale transition transition-duration transition-property transition-transform box-shadow
|
36
|
+
)
|
37
|
+
|
38
|
+
# A list of classes that will be extracted into mixins
|
39
|
+
# Only the top-level selectors of form .CLASS { ... } are extracted. CLASS must not be used in any other rule definition.
|
40
|
+
# This is a work-around for libsass @extend issues
|
41
|
+
CLASSES_TO_MIXINS = %w(
|
42
|
+
list-unstyled form-inline
|
43
|
+
)
|
44
|
+
|
45
|
+
# Convert a snippet of bootstrap LESS to Scss
|
46
|
+
def convert_less(less)
|
47
|
+
less = convert_to_scss(less)
|
48
|
+
less = yield(less) if block_given?
|
49
|
+
less
|
50
|
+
end
|
51
|
+
|
52
|
+
def shared_mixins
|
53
|
+
@shared_mixins ||= begin
|
54
|
+
log_status ' Reading shared mixins from mixins.less'
|
55
|
+
CLASSES_TO_MIXINS + read_mixins(read_files('less', bootstrap_less_files.grep(/mixins\//)).values.join("\n"),
|
56
|
+
nested: NESTED_MIXINS)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_stylesheet_assets
|
61
|
+
log_status 'Processing stylesheets...'
|
62
|
+
files = read_files('less', bootstrap_less_files)
|
63
|
+
save_to = @save_to[:scss]
|
64
|
+
|
65
|
+
log_status ' Converting LESS files to Scss:'
|
66
|
+
files.each do |name, file|
|
67
|
+
log_processing name
|
68
|
+
# apply common conversions
|
69
|
+
file = convert_less(file)
|
70
|
+
file = replace_all file, %r{// stylelint-disable.*?\n+}, '', optional: true
|
71
|
+
if name.start_with?('mixins/')
|
72
|
+
file = varargify_mixin_definitions(file, *VARARG_MIXINS)
|
73
|
+
%w(responsive-(in)?visibility input-size text-emphasis-variant bg-variant).each do |mixin|
|
74
|
+
file = parameterize_mixin_parent_selector file, mixin if file =~ /#{mixin}/
|
75
|
+
end
|
76
|
+
NESTED_MIXINS.each do |sel, name|
|
77
|
+
file = flatten_mixins(file, sel, name) if /#{Regexp.escape(sel)}/ =~ file
|
78
|
+
end
|
79
|
+
file = replace_all file, /(?<=[.-])\$state/, '#{$state}' if file =~ /[.-]\$state/
|
80
|
+
end
|
81
|
+
case name
|
82
|
+
when 'mixins/buttons.less'
|
83
|
+
file = replace_all file, /(\.dropdown-toggle)&/, '&\1'
|
84
|
+
when 'mixins/list-group.less'
|
85
|
+
file = replace_rules(file, ' .list-group-item-') { |rule| extract_nested_rule rule, 'a&' }
|
86
|
+
when 'mixins/gradients.less'
|
87
|
+
file = replace_ms_filters(file)
|
88
|
+
file = deinterpolate_vararg_mixins(file)
|
89
|
+
when 'mixins/vendor-prefixes.less'
|
90
|
+
# remove second scale mixins as this is handled via vararg in the first one
|
91
|
+
file = replace_rules(file, Regexp.escape('@mixin scale($ratioX, $ratioY...)')) { '' }
|
92
|
+
when 'mixins/grid-framework.less'
|
93
|
+
file = convert_grid_mixins file
|
94
|
+
when 'component-animations.less'
|
95
|
+
file = extract_nested_rule file, "#{SELECTOR_RE}&\\.in"
|
96
|
+
when 'responsive-utilities.less'
|
97
|
+
file = apply_mixin_parent_selector file, '\.(?:visible|hidden)'
|
98
|
+
when 'variables.less'
|
99
|
+
file = insert_default_vars(file)
|
100
|
+
file = ['$bootstrap-sass-asset-helper: false !default;', file].join("\n")
|
101
|
+
file = replace_all file, %r{(\$icon-font-path): \s*"(.*)" (!default);}, "\n" + unindent(<<-SCSS, 14)
|
102
|
+
// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
|
103
|
+
// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
|
104
|
+
\\1: if($bootstrap-sass-asset-helper, "bootstrap/", "\\2bootstrap/") \\3;
|
105
|
+
SCSS
|
106
|
+
when 'breadcrumbs.less'
|
107
|
+
file = replace_all file, /(.*)(\\00a0)/, unindent(<<-SCSS, 8) + "\\1\#{$nbsp}"
|
108
|
+
// [converter] Workaround for https://github.com/sass/libsass/issues/1115
|
109
|
+
$nbsp: "\\2";
|
110
|
+
SCSS
|
111
|
+
when 'close.less'
|
112
|
+
# extract .close { button& {...} } rule
|
113
|
+
file = extract_nested_rule file, 'button&'
|
114
|
+
when 'dropdowns.less'
|
115
|
+
file = replace_all file, /@extend \.dropdown-menu-right;/, 'right: 0; left: auto;'
|
116
|
+
file = replace_all file, /@extend \.dropdown-menu-left;/, 'left: 0; right: auto;'
|
117
|
+
when 'forms.less'
|
118
|
+
file = extract_nested_rule file, 'textarea&'
|
119
|
+
file = apply_mixin_parent_selector(file, '\.input-(?:sm|lg)')
|
120
|
+
file = replace_rules file, /\.form-group-(?:sm|lg)/ do |rule|
|
121
|
+
apply_mixin_parent_selector rule, '.form-control'
|
122
|
+
end
|
123
|
+
when 'navbar.less'
|
124
|
+
file = replace_all file, /(\s*)\.navbar-(right|left)\s*\{\s*@extend\s*\.pull-(right|left);\s*/, "\\1.navbar-\\2 {\\1 float: \\2 !important;\\1"
|
125
|
+
when 'tables.less'
|
126
|
+
file = replace_all file, /(@include\s*table-row-variant\()(\w+)/, "\\1'\\2'"
|
127
|
+
when 'thumbnails.less', 'labels.less', 'badges.less', 'buttons.less'
|
128
|
+
file = extract_nested_rule file, 'a&'
|
129
|
+
when 'glyphicons.less'
|
130
|
+
file = replace_rules(file, /\s*@font-face/) { |rule| replace_asset_url rule, :font }
|
131
|
+
when 'type.less'
|
132
|
+
file = apply_mixin_parent_selector(file, '\.(text|bg)-(success|primary|info|warning|danger)')
|
133
|
+
# .bg-primary will not get patched automatically as it includes an additional rule. fudge for now
|
134
|
+
file = replace_all(file, " @include bg-variant($brand-primary);\n}", "}\n@include bg-variant('.bg-primary', $brand-primary);")
|
135
|
+
end
|
136
|
+
|
137
|
+
path = File.join save_to, name.sub(/\.less$/, '.scss')
|
138
|
+
path = File.join File.dirname(path), '_' + File.basename(path)
|
139
|
+
save_file(path, file)
|
140
|
+
log_processed File.basename(path)
|
141
|
+
end
|
142
|
+
|
143
|
+
# move bootstrap/_bootstrap.scss to _bootstrap.scss adjusting import paths
|
144
|
+
main_from = "#{save_to}/_bootstrap.scss"
|
145
|
+
main_to = File.expand_path("#{save_to}/../_bootstrap.scss")
|
146
|
+
save_file main_to, File.read(main_from).gsub(/ "/, ' "bootstrap/')
|
147
|
+
File.delete(main_from)
|
148
|
+
|
149
|
+
# generate variables template
|
150
|
+
save_file 'templates/project/_bootstrap-variables.sass',
|
151
|
+
"// Override Bootstrap variables here (defaults from bootstrap-sass v#{Bootstrap::VERSION}):\n\n" +
|
152
|
+
File.read("#{save_to}/_variables.scss").lines[1..-1].join.gsub(/^(?=\$)/, '// ').gsub(/ !default;/, '')
|
153
|
+
end
|
154
|
+
|
155
|
+
def bootstrap_less_files
|
156
|
+
@bootstrap_less_files ||= get_paths_by_type('less', /\.less$/)
|
157
|
+
end
|
158
|
+
|
159
|
+
# apply general less to scss conversion
|
160
|
+
def convert_to_scss(file)
|
161
|
+
# get local mixin names before converting the definitions
|
162
|
+
mixins = shared_mixins + read_mixins(file)
|
163
|
+
file = replace_vars(file)
|
164
|
+
file = replace_mixin_definitions(file)
|
165
|
+
file = replace_mixins(file, mixins)
|
166
|
+
file = extract_mixins_from_selectors(file, CLASSES_TO_MIXINS.inject({}) { |h, cl| h.update(".#{cl}" => cl) })
|
167
|
+
file = replace_spin(file)
|
168
|
+
file = replace_fadein(file)
|
169
|
+
file = replace_image_urls(file)
|
170
|
+
file = replace_escaping(file)
|
171
|
+
file = convert_less_ampersand(file)
|
172
|
+
file = deinterpolate_vararg_mixins(file)
|
173
|
+
file = replace_calculation_semantics(file)
|
174
|
+
file = replace_file_imports(file)
|
175
|
+
file = wrap_at_groups_with_at_root(file)
|
176
|
+
file = replace_division(file)
|
177
|
+
file
|
178
|
+
end
|
179
|
+
|
180
|
+
def wrap_at_groups_with_at_root(file)
|
181
|
+
replace_rules(file, /@(?:font-face|-ms-viewport)/) { |rule, _pos|
|
182
|
+
%Q(@at-root {\n#{indent rule, 2}\n})
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
def replace_division(less)
|
187
|
+
re = %r{
|
188
|
+
(?<expression>
|
189
|
+
(?<callee>[[:alpha:]\.]+)?
|
190
|
+
\(
|
191
|
+
(?:
|
192
|
+
(?>
|
193
|
+
(?<dividend>
|
194
|
+
[^()/]+
|
195
|
+
|
|
196
|
+
\([^/]+\)
|
197
|
+
)
|
198
|
+
\s+
|
199
|
+
/
|
200
|
+
\s+
|
201
|
+
(?<divisor>
|
202
|
+
[^()/]+
|
203
|
+
|
|
204
|
+
\([^/]+\)
|
205
|
+
)
|
206
|
+
)
|
207
|
+
|
|
208
|
+
\g<expression>
|
209
|
+
)
|
210
|
+
\)
|
211
|
+
)
|
212
|
+
}x
|
213
|
+
return less if less !~ re
|
214
|
+
"@use \"sass:math\";\n" + less.gsub(re) do
|
215
|
+
named_captures = $~.named_captures
|
216
|
+
callee = named_captures['callee']
|
217
|
+
dividend = named_captures['dividend']
|
218
|
+
divisor = named_captures['divisor']
|
219
|
+
expression = "math.div(#{dividend}, #{divisor})"
|
220
|
+
callee.nil? ? expression : "#{callee}(#{expression})"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def sass_fn_exists(fn)
|
225
|
+
%Q{(#{fn}("") != unquote('#{fn}("")'))}
|
226
|
+
end
|
227
|
+
|
228
|
+
def replace_asset_url(rule, type)
|
229
|
+
replace_all rule, /url\((.*?)\)/, "url(if($bootstrap-sass-asset-helper, twbs-#{type}-path(\\1), \\1))"
|
230
|
+
end
|
231
|
+
|
232
|
+
# convert recursively evaluated selector $list to @for loop
|
233
|
+
def mixin_all_grid_columns(css, selector: raise('pass class'), from: 1, to: raise('pass to'))
|
234
|
+
mxn_def = css.each_line.first.strip
|
235
|
+
# inject local variables as default arguments
|
236
|
+
# this is to avoid overwriting outer variables with the same name with Sass <= 3.3
|
237
|
+
# see also: https://github.com/twbs/bootstrap-sass/issues/636
|
238
|
+
locals = <<-SASS.strip
|
239
|
+
$i: #{from}, $list: "#{selector}"
|
240
|
+
SASS
|
241
|
+
mxn_def.sub!(/(\(?)(\)\s*\{)/) { "#{$1}#{', ' if $1.empty?}#{locals}#{$2}" }
|
242
|
+
step_body = (css =~ /\$list \{\n(.*?)\n[ ]*\}/m) && $1
|
243
|
+
<<-SASS
|
244
|
+
// [converter] This is defined recursively in LESS, but Sass supports real loops
|
245
|
+
#{mxn_def}
|
246
|
+
@for $i from (#{from} + 1) through #{to} {
|
247
|
+
$list: "\#{$list}, #{selector}";
|
248
|
+
}
|
249
|
+
\#{$list} {
|
250
|
+
#{unindent step_body, 2}
|
251
|
+
}
|
252
|
+
}
|
253
|
+
SASS
|
254
|
+
end
|
255
|
+
|
256
|
+
# convert grid mixins LESS when => Sass @if
|
257
|
+
def convert_grid_mixins(file)
|
258
|
+
file = replace_rules file, /@mixin make-grid-columns/, comments: false do |css, pos|
|
259
|
+
mixin_all_grid_columns css, selector: '.col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}', to: '$grid-columns'
|
260
|
+
end
|
261
|
+
file = replace_rules file, /@mixin float-grid-columns/, comments: false do |css, pos|
|
262
|
+
mixin_all_grid_columns css, selector: '.col-#{$class}-#{$i}', to: '$grid-columns'
|
263
|
+
end
|
264
|
+
file = replace_rules file, /@mixin calc-grid-column/ do |css|
|
265
|
+
css = indent css.gsub(/.*when (.*?) {/, '@if \1 {').gsub(/(\$[\w-]+)\s+=\s+(\w+)/, '\1 == \2').gsub(/(?<=-)(\$[a-z]+)/, '#{\1}')
|
266
|
+
if css =~ /== width/
|
267
|
+
css = "@mixin calc-grid-column($index, $class, $type) {\n#{css}"
|
268
|
+
elsif css =~ /== offset/
|
269
|
+
css += "\n}"
|
270
|
+
end
|
271
|
+
css
|
272
|
+
end
|
273
|
+
file = replace_rules file, /@mixin loop-grid-columns/ do |css|
|
274
|
+
unindent <<-SASS, 8
|
275
|
+
// [converter] This is defined recursively in LESS, but Sass supports real loops
|
276
|
+
@mixin loop-grid-columns($columns, $class, $type) {
|
277
|
+
@for $i from 0 through $columns {
|
278
|
+
@include calc-grid-column($i, $class, $type);
|
279
|
+
}
|
280
|
+
}
|
281
|
+
SASS
|
282
|
+
end
|
283
|
+
file
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
# We need to keep a list of shared mixin names in order to convert the includes correctly
|
288
|
+
# Before doing any processing we read shared mixins from a file
|
289
|
+
# If a mixin is nested, it gets prefixed in the list (e.g. #gradient > .horizontal to 'gradient-horizontal')
|
290
|
+
def read_mixins(mixins_file, nested: {})
|
291
|
+
mixins = get_mixin_names(mixins_file, silent: true)
|
292
|
+
nested.each do |selector, prefix|
|
293
|
+
# we use replace_rules without replacing anything just to use the parsing algorithm
|
294
|
+
replace_rules(mixins_file, selector) { |rule|
|
295
|
+
mixins += get_mixin_names(unindent(unwrap_rule_block(rule)), silent: true).map { |name| "#{prefix}-#{name}" }
|
296
|
+
rule
|
297
|
+
}
|
298
|
+
end
|
299
|
+
mixins.uniq!
|
300
|
+
mixins.sort!
|
301
|
+
log_file_info "mixins: #{mixins * ', '}" unless mixins.empty?
|
302
|
+
mixins
|
303
|
+
end
|
304
|
+
|
305
|
+
def get_mixin_names(file, opts = {})
|
306
|
+
names = get_css_selectors(file).join("\n" * 2).scan(/^\.([\w-]+)\(#{LESS_MIXIN_DEF_ARGS_RE}\)(?: when.*?)?[ ]*\{/).map(&:first).uniq.sort
|
307
|
+
log_file_info "mixin defs: #{names * ', '}" unless opts[:silent] || names.empty?
|
308
|
+
names
|
309
|
+
end
|
310
|
+
|
311
|
+
# margin: a -b
|
312
|
+
# LESS: sets 2 values
|
313
|
+
# Sass: sets 1 value (a-b)
|
314
|
+
# This wraps a and -b so they evaluates to 2 values in Sass
|
315
|
+
def replace_calculation_semantics(file)
|
316
|
+
# split_prop_val.call('(@navbar-padding-vertical / 2) -@navbar-padding-horizontal')
|
317
|
+
# #=> ["(navbar-padding-vertical / 2)", "-navbar-padding-horizontal"]
|
318
|
+
split_prop_val = proc { |val|
|
319
|
+
s = CharStringScanner.new(val)
|
320
|
+
r = []
|
321
|
+
buff = ''
|
322
|
+
d = 0
|
323
|
+
prop_char = %r([\$\w\-/\*\+%!])
|
324
|
+
while (token = s.scan_next(/([\)\(]|\s+|#{prop_char}+)/))
|
325
|
+
buff << token
|
326
|
+
case token
|
327
|
+
when '('
|
328
|
+
d += 1
|
329
|
+
when ')'
|
330
|
+
d -= 1
|
331
|
+
if d == 0
|
332
|
+
r << buff
|
333
|
+
buff = ''
|
334
|
+
end
|
335
|
+
when /\s/
|
336
|
+
if d == 0 && !buff.strip.empty?
|
337
|
+
r << buff
|
338
|
+
buff = ''
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
r << buff unless buff.empty?
|
343
|
+
r.map(&:strip)
|
344
|
+
}
|
345
|
+
|
346
|
+
replace_rules file do |rule|
|
347
|
+
replace_properties rule do |props|
|
348
|
+
props.gsub /(?<!\w)([\w-]+):(.*?);/ do |m|
|
349
|
+
prop, vals = $1, split_prop_val.call($2)
|
350
|
+
next m unless vals.length >= 2 && vals.any? { |v| v =~ /^[\+\-]\$/ }
|
351
|
+
transformed = vals.map { |v| v.strip =~ %r(^\(.*\)$) ? v : "(#{v})" }
|
352
|
+
log_transform "property #{prop}: #{transformed * ' '}", from: 'wrap_calculation'
|
353
|
+
"#{prop}: #{transformed * ' '};"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# @import "file.less" to "#{target_path}file;"
|
360
|
+
def replace_file_imports(less, target_path = '')
|
361
|
+
less.gsub %r([@\$]import ["|']([\w\-/]+).less["|'];),
|
362
|
+
%Q(@import "#{target_path}\\1";)
|
363
|
+
end
|
364
|
+
|
365
|
+
def replace_all(file, regex, replacement = nil, optional: false, &block)
|
366
|
+
log_transform regex, replacement
|
367
|
+
new_file = file.gsub(regex, replacement, &block)
|
368
|
+
raise "replace_all #{regex}, #{replacement} NO MATCH" if !optional && file == new_file
|
369
|
+
new_file
|
370
|
+
end
|
371
|
+
|
372
|
+
# @mixin a() { tr& { color:white } }
|
373
|
+
# to:
|
374
|
+
# @mixin a($parent) { tr#{$parent} { color: white } }
|
375
|
+
def parameterize_mixin_parent_selector(file, rule_sel)
|
376
|
+
log_transform rule_sel
|
377
|
+
param = '$parent'
|
378
|
+
replace_rules(file, '^\s*@mixin\s*' + rule_sel) do |mxn_css|
|
379
|
+
mxn_css.sub! /(?=@mixin)/, "// [converter] $parent hack\n"
|
380
|
+
# insert param into mixin def
|
381
|
+
mxn_css.sub!(/(@mixin [\w-]+)\(([\$\w\-,\s]*)\)/) { "#{$1}(#{param}#{', ' if $2 && !$2.empty?}#{$2})" }
|
382
|
+
# wrap properties in #{$parent} { ... }
|
383
|
+
replace_properties(mxn_css) { |props|
|
384
|
+
next props if props.strip.empty?
|
385
|
+
spacer = ' ' * indent_width(props)
|
386
|
+
"#{spacer}\#{#{param}} {\n#{indent(props.sub(/\s+\z/, ''), 2)}\n#{spacer}}"
|
387
|
+
}
|
388
|
+
# change nested& rules to nested#{$parent}
|
389
|
+
replace_rules(mxn_css, /.*&[ ,:]/) { |rule| replace_in_selector rule, /&/, "\#{#{param}}" }
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
# extracts rule immediately after it's parent, and adjust the selector
|
394
|
+
# .x { textarea& { ... }}
|
395
|
+
# to:
|
396
|
+
# .x { ... }
|
397
|
+
# textarea.x { ... }
|
398
|
+
def extract_nested_rule(file, selector, new_selector = nil)
|
399
|
+
matches = []
|
400
|
+
# first find the rules, and remove them
|
401
|
+
file = replace_rules(file, "\s*#{selector}", comments: true) { |rule, pos, css|
|
402
|
+
new_sel = new_selector || "#{get_selector(rule).gsub(/&/, selector_for_pos(css, pos.begin))}"
|
403
|
+
matches << [rule, pos, new_sel]
|
404
|
+
indent "// [converter] extracted #{get_selector(rule)} to #{new_sel}".tr("\n", ' ').squeeze(' '), indent_width(rule)
|
405
|
+
}
|
406
|
+
raise "extract_nested_rule: no such selector: #{selector}" if matches.empty?
|
407
|
+
# replace rule selector with new_selector
|
408
|
+
matches.each do |m|
|
409
|
+
m[0].sub! /(#{COMMENT_RE}*)^(\s*).*?(\s*){/m, "\\1\\2#{m[2]}\\3{"
|
410
|
+
log_transform selector, m[2]
|
411
|
+
end
|
412
|
+
replace_substrings_at file,
|
413
|
+
matches.map { |_, pos| close_brace_pos(file, pos.begin, 1) + 1 },
|
414
|
+
matches.map { |rule, _| "\n\n" + unindent(rule) }
|
415
|
+
end
|
416
|
+
|
417
|
+
# .visible-sm { @include responsive-visibility() }
|
418
|
+
# to:
|
419
|
+
# @include responsive-visibility('.visible-sm')
|
420
|
+
def apply_mixin_parent_selector(file, rule_sel)
|
421
|
+
log_transform rule_sel
|
422
|
+
replace_rules file, '\s*' + rule_sel, comments: false do |rule, rule_pos, css|
|
423
|
+
body = unwrap_rule_block(rule.dup).strip
|
424
|
+
next rule unless body =~ /^@include \w+/m || body =~ /^@media/ && body =~ /\{\s*@include/
|
425
|
+
rule =~ /(#{COMMENT_RE}*)([#{SELECTOR_CHAR}\s*]+?)#{RULE_OPEN_BRACE_RE}/
|
426
|
+
cmt, sel = $1, $2.strip
|
427
|
+
# take one up selector chain if this is an &. selector
|
428
|
+
if sel.start_with?('&')
|
429
|
+
parent_sel = selector_for_pos(css, rule_pos.begin)
|
430
|
+
sel = parent_sel + sel[1..-1]
|
431
|
+
end
|
432
|
+
# unwrap, and replace @include
|
433
|
+
unindent unwrap_rule_block(rule).gsub(/(@include [\w-]+)\(?([\$\w\-,\s]*)\)?/) {
|
434
|
+
name, args = $1, $2
|
435
|
+
sel.gsub(/\s+/, ' ').split(/,\s*/ ).map { |sel_part|
|
436
|
+
"#{cmt}#{name}('#{sel_part}'#{', ' if args && !args.empty?}#{args})"
|
437
|
+
}.join(";\n")
|
438
|
+
}
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# #gradient > { @mixin horizontal ... }
|
443
|
+
# to:
|
444
|
+
# @mixin gradient-horizontal
|
445
|
+
def flatten_mixins(file, container, prefix)
|
446
|
+
log_transform container, prefix
|
447
|
+
replace_rules file, Regexp.escape(container) do |mixins_css|
|
448
|
+
unindent unwrap_rule_block(mixins_css).gsub(/@mixin\s*([\w-]+)/, "@mixin #{prefix}-\\1")
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# .btn { ... } -> @mixin btn { ... }; .btn { @include btn }
|
453
|
+
def extract_mixins_from_selectors(file, selectors_to_mixins)
|
454
|
+
selectors_to_mixins.each do |selector, mixin|
|
455
|
+
file = replace_rules file, Regexp.escape(selector), prefix: false do |selector_css|
|
456
|
+
log_transform "#{selector} { ... } -> @mixin #{mixin} { ... }; #{selector} { @include #{mixin} } ", from: 'extract_mixins_from_selectors'
|
457
|
+
<<-SCSS
|
458
|
+
// [converter] extracted from `#{selector}` for libsass compatibility
|
459
|
+
@mixin #{mixin} {#{unwrap_rule_block(selector_css)}
|
460
|
+
}
|
461
|
+
// [converter] extracted as `@mixin #{mixin}` for libsass compatibility
|
462
|
+
#{selector} {
|
463
|
+
@include #{mixin};
|
464
|
+
}
|
465
|
+
SCSS
|
466
|
+
end
|
467
|
+
end
|
468
|
+
file
|
469
|
+
end
|
470
|
+
|
471
|
+
# @include and @extend from LESS:
|
472
|
+
# .mixin() -> @include mixin()
|
473
|
+
# #scope > .mixin() -> @include scope-mixin()
|
474
|
+
# &:extend(.mixin all) -> @include mixin()
|
475
|
+
def replace_mixins(less, mixin_names)
|
476
|
+
mixin_pattern = /(?<=^|\s)((?:[#|\.][\w-]+\s*>\s*)*)\.([\w-]+)\((.*)\)(?!\s\{)/
|
477
|
+
|
478
|
+
less = less.gsub(mixin_pattern) do |_|
|
479
|
+
scope, name, args = $1, $2, $3
|
480
|
+
scope = scope.scan(/[\w-]+/).join('-') + '-' unless scope.empty?
|
481
|
+
args = "(#{args.tr(';', ',')})" unless args.empty?
|
482
|
+
if name && mixin_names.include?("#{scope}#{name}")
|
483
|
+
"@include #{scope}#{name}#{args}"
|
484
|
+
else
|
485
|
+
"@extend .#{scope}#{name}"
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
less.gsub /&:extend\((#{SELECTOR_RE})(?: all)?\)/ do
|
490
|
+
selector = $1
|
491
|
+
selector =~ /\.([\w-]+)/
|
492
|
+
mixin = $1
|
493
|
+
if mixin && mixin_names.include?(mixin)
|
494
|
+
"@include #{mixin}"
|
495
|
+
else
|
496
|
+
"@extend #{selector}"
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# change Microsoft filters to Sass calling convention
|
502
|
+
def replace_ms_filters(file)
|
503
|
+
log_transform
|
504
|
+
file.gsub(
|
505
|
+
/filter: e\(%\("progid:DXImageTransform.Microsoft.gradient\(startColorstr='%d', endColorstr='%d', GradientType=(\d)\)", ?argb\(([\-$\w]+)\), ?argb\(([\-$\w]+)\)\)\);/,
|
506
|
+
%Q(filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='\#{ie-hex-str(\\2)}', endColorstr='\#{ie-hex-str(\\3)}', GradientType=\\1);)
|
507
|
+
)
|
508
|
+
end
|
509
|
+
|
510
|
+
# unwraps topmost rule block
|
511
|
+
# #sel { a: b; }
|
512
|
+
# to:
|
513
|
+
# a: b;
|
514
|
+
def unwrap_rule_block(css)
|
515
|
+
css[(css =~ RULE_OPEN_BRACE_RE) + 1..-1].sub(/\n?}\s*\z/m, '')
|
516
|
+
end
|
517
|
+
|
518
|
+
def replace_mixin_definitions(less)
|
519
|
+
less.gsub(/^(\s*)\.([\w-]+\(.*\))(\s*\{)/) { |match|
|
520
|
+
"#{$1}@mixin #{$2.tr(';', ',')}#{$3}".sub(/,\)/, ')')
|
521
|
+
}
|
522
|
+
end
|
523
|
+
|
524
|
+
def replace_vars(less)
|
525
|
+
less = less.dup
|
526
|
+
# skip header comment
|
527
|
+
less =~ %r(\A/\*(.*?)\*/)m
|
528
|
+
from = $~ ? $~.to_s.length : 0
|
529
|
+
less[from..-1] = less[from..-1].
|
530
|
+
gsub(/(?!@mixin|@media|@page|@keyframes|@font-face|@-\w)@/, '$').
|
531
|
+
# variables that would be ignored by gsub above: e.g. @page-header-border-color
|
532
|
+
gsub(/@(page[\w-]+)/, '$\1')
|
533
|
+
less
|
534
|
+
end
|
535
|
+
|
536
|
+
def replace_spin(less)
|
537
|
+
less.gsub(/(?![\-$@.])spin(?!-)/, 'adjust-hue')
|
538
|
+
end
|
539
|
+
|
540
|
+
def replace_fadein(less)
|
541
|
+
less.gsub(/(?![\-$@.])fadein\((.*?),\s*(.*?)%\)/) { "fade_in(#{$1}, #{$2.to_i / 100.0})" }
|
542
|
+
end
|
543
|
+
|
544
|
+
def replace_image_urls(less)
|
545
|
+
less.gsub(/background-image: url\("?(.*?)"?\);/) { |s| replace_asset_url s, :image }
|
546
|
+
end
|
547
|
+
|
548
|
+
def replace_escaping(less)
|
549
|
+
less = less.gsub(/~"([^"]+)"/, '\1').gsub(/~'([^']+)'/, '\1') # Get rid of ~"" escape
|
550
|
+
less.gsub!(/\$\{([^}]+)\}/, '$\1') # Get rid of @{} escape
|
551
|
+
# interpolate variables in strings, e.g. url("$file-1x") => url("#{$file-1x}")
|
552
|
+
less.gsub!(/"[^"\n]*"/) { |str| str.gsub(/\$[^"\n$.\\]+/, '#{\0}') }
|
553
|
+
less.gsub(/(\W)e\(%\("?([^"]*)"?\)\)/, '\1\2') # Get rid of e(%("")) escape
|
554
|
+
end
|
555
|
+
|
556
|
+
def insert_default_vars(scss)
|
557
|
+
log_transform
|
558
|
+
scss.gsub(/^(\$.+);/, '\1 !default;')
|
559
|
+
end
|
560
|
+
|
561
|
+
# Converts &-
|
562
|
+
def convert_less_ampersand(less)
|
563
|
+
regx = /^\.badge\s*\{[\s\/\w\(\)]+(&{1}-{1})\w.*?^}$/m
|
564
|
+
|
565
|
+
tmp = ''
|
566
|
+
less.scan(/^(\s*&)(-[\w\[\]]+\s*\{.+})$/) do |ampersand, css|
|
567
|
+
tmp << ".badge#{css}\n"
|
568
|
+
end
|
569
|
+
|
570
|
+
less.gsub(regx, tmp)
|
571
|
+
end
|
572
|
+
|
573
|
+
# unindent by n spaces
|
574
|
+
def unindent(txt, n = 2)
|
575
|
+
txt.gsub /^[ ]{#{n}}/, ''
|
576
|
+
end
|
577
|
+
|
578
|
+
# indent by n spaces
|
579
|
+
def indent(txt, n = 2)
|
580
|
+
spaces = ' ' * n
|
581
|
+
txt.gsub /^/, spaces
|
582
|
+
end
|
583
|
+
|
584
|
+
# get indent length from the first line of txt
|
585
|
+
def indent_width(txt)
|
586
|
+
txt.match(/\A\s*/).to_s.length
|
587
|
+
end
|
588
|
+
|
589
|
+
# @mixin transition($transition) {
|
590
|
+
# to:
|
591
|
+
# @mixin transition($transition...) {
|
592
|
+
def varargify_mixin_definitions(scss, *mixins)
|
593
|
+
scss = scss.dup
|
594
|
+
replaced = []
|
595
|
+
mixins.each do |mixin|
|
596
|
+
if scss.gsub! /(@mixin\s*#{Regexp.quote(mixin)})\((#{SCSS_MIXIN_DEF_ARGS_RE})\)/, '\1(\2...)'
|
597
|
+
replaced << mixin
|
598
|
+
end
|
599
|
+
end
|
600
|
+
log_transform *replaced unless replaced.empty?
|
601
|
+
scss
|
602
|
+
end
|
603
|
+
|
604
|
+
# @include transition(#{border-color ease-in-out .15s, box-shadow ease-in-out .15s})
|
605
|
+
# to
|
606
|
+
# @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s)
|
607
|
+
def deinterpolate_vararg_mixins(scss)
|
608
|
+
scss = scss.dup
|
609
|
+
VARARG_MIXINS.each do |mixin|
|
610
|
+
if scss.gsub! /(@include\s*#{Regexp.quote(mixin)})\(\s*\#\{([^}]+)\}\s*\)/, '\1(\2)'
|
611
|
+
log_transform mixin
|
612
|
+
end
|
613
|
+
end
|
614
|
+
scss
|
615
|
+
end
|
616
|
+
|
617
|
+
# get full selector for rule_block
|
618
|
+
def get_selector(rule_block)
|
619
|
+
sel = /^\s*(#{SELECTOR_RE}?)\s*\{/.match(rule_block) && $1 && $1.strip
|
620
|
+
sel.sub /\s*\{\n\s.*/m, ''
|
621
|
+
end
|
622
|
+
|
623
|
+
# replace CSS rule blocks matching rule_prefix with yield(rule_block, rule_pos)
|
624
|
+
# will also include immediately preceding comments in rule_block
|
625
|
+
#
|
626
|
+
# option :comments -- include immediately preceding comments in rule_block
|
627
|
+
#
|
628
|
+
# replace_rules(".a{ \n .b{} }", '.b') { |rule, pos| ">#{rule}<" } #=> ".a{ \n >.b{}< }"
|
629
|
+
def replace_rules(less, selector = SELECTOR_RE, options = {}, &block)
|
630
|
+
options = {prefix: true, comments: true}.merge(options || {})
|
631
|
+
less = less.dup
|
632
|
+
s = CharStringScanner.new(less)
|
633
|
+
rule_re = if options[:prefix]
|
634
|
+
/(?:#{selector}[#{SELECTOR_CHAR})=(\s]*?#{RULE_OPEN_BRACE_RE})/
|
635
|
+
else
|
636
|
+
/#{selector}[\s]*#{RULE_OPEN_BRACE_RE}/
|
637
|
+
end
|
638
|
+
rule_start_re = if options[:comments]
|
639
|
+
/(?:#{COMMENT_RE}*)^#{rule_re}/
|
640
|
+
else
|
641
|
+
/^#{rule_re}/
|
642
|
+
end
|
643
|
+
|
644
|
+
positions = []
|
645
|
+
while (rule_start = s.scan_next(rule_start_re))
|
646
|
+
pos = s.pos
|
647
|
+
positions << (pos - rule_start.length..close_brace_pos(less, pos - 1))
|
648
|
+
end
|
649
|
+
replace_substrings_at(less, positions, &block)
|
650
|
+
less
|
651
|
+
end
|
652
|
+
|
653
|
+
# Get a all top-level selectors (with {)
|
654
|
+
def get_css_selectors(css, opts = {})
|
655
|
+
s = CharStringScanner.new(css)
|
656
|
+
selectors = []
|
657
|
+
while s.scan_next(RULE_OPEN_BRACE_RE)
|
658
|
+
brace_pos = s.pos
|
659
|
+
def_pos = css_def_pos(css, brace_pos+1, -1)
|
660
|
+
sel = css[def_pos.begin..brace_pos - 1].dup
|
661
|
+
sel.strip! if opts[:strip]
|
662
|
+
selectors << sel
|
663
|
+
sel.dup.strip
|
664
|
+
s.pos = close_brace_pos(css, brace_pos, 1) + 1
|
665
|
+
end
|
666
|
+
selectors
|
667
|
+
end
|
668
|
+
|
669
|
+
# replace in the top-level selector
|
670
|
+
# replace_in_selector('a {a: {a: a} } a {}', /a/, 'b') => 'b {a: {a: a} } b {}'
|
671
|
+
def replace_in_selector(css, pattern, sub)
|
672
|
+
# scan for selector positions in css
|
673
|
+
s = CharStringScanner.new(css)
|
674
|
+
prev_pos = 0
|
675
|
+
sel_pos = []
|
676
|
+
while (brace = s.scan_next(RULE_OPEN_BRACE_RE))
|
677
|
+
pos = s.pos
|
678
|
+
sel_pos << (prev_pos .. pos - 1)
|
679
|
+
s.pos = close_brace_pos(css, s.pos - 1) + 1
|
680
|
+
prev_pos = pos
|
681
|
+
end
|
682
|
+
replace_substrings_at(css, sel_pos) { |s| s.gsub(pattern, sub) }
|
683
|
+
end
|
684
|
+
|
685
|
+
|
686
|
+
# replace first level properties in the css with yields
|
687
|
+
# replace_properties("a { color: white }") { |props| props.gsub 'white', 'red' }
|
688
|
+
def replace_properties(css, &block)
|
689
|
+
s = CharStringScanner.new(css)
|
690
|
+
s.skip_until /#{RULE_OPEN_BRACE_RE}\n?/
|
691
|
+
from = s.pos
|
692
|
+
m = s.scan_next(/\s*#{SELECTOR_RE}#{RULE_OPEN_BRACE_RE}/) || s.scan_next(/\s*#{RULE_CLOSE_BRACE_RE}/)
|
693
|
+
to = s.pos - m.length - 1
|
694
|
+
replace_substrings_at css, [(from .. to)], &block
|
695
|
+
end
|
696
|
+
|
697
|
+
|
698
|
+
# immediate selector of css at pos
|
699
|
+
def selector_for_pos(css, pos, depth = -1)
|
700
|
+
css[css_def_pos(css, pos, depth)].dup.strip
|
701
|
+
end
|
702
|
+
|
703
|
+
# get the pos of css def at pos (search backwards)
|
704
|
+
def css_def_pos(css, pos, depth = -1)
|
705
|
+
to = open_brace_pos(css, pos, depth)
|
706
|
+
prev_def = to - (css[0..to].reverse.index(RULE_CLOSE_BRACE_RE_REVERSE) || to) + 1
|
707
|
+
from = prev_def + 1 + (css[prev_def + 1..-1] =~ %r(^\s*[^\s/]))
|
708
|
+
(from..to - 1)
|
709
|
+
end
|
710
|
+
|
711
|
+
# next matching brace for brace at from
|
712
|
+
def close_brace_pos(css, from, depth = 0)
|
713
|
+
s = CharStringScanner.new(css[from..-1])
|
714
|
+
while (b = s.scan_next(BRACE_RE))
|
715
|
+
depth += (b == '}' ? -1 : +1)
|
716
|
+
break if depth.zero?
|
717
|
+
end
|
718
|
+
raise "match not found for {" unless depth.zero?
|
719
|
+
from + s.pos - 1
|
720
|
+
end
|
721
|
+
|
722
|
+
# opening brace position from +from+ (search backwards)
|
723
|
+
def open_brace_pos(css, from, depth = 0)
|
724
|
+
s = CharStringScanner.new(css[0..from].reverse)
|
725
|
+
while (b = s.scan_next(BRACE_RE_REVERSE))
|
726
|
+
depth += (b == '{' ? +1 : -1)
|
727
|
+
break if depth.zero?
|
728
|
+
end
|
729
|
+
raise "matching { brace not found" unless depth.zero?
|
730
|
+
from - s.pos + 1
|
731
|
+
end
|
732
|
+
|
733
|
+
# insert substitutions into text at positions (Range or Integer)
|
734
|
+
# substitutions can be passed as array or as yields from the &block called with |substring, position, text|
|
735
|
+
# position is a range (begin..end)
|
736
|
+
def replace_substrings_at(text, positions, replacements = nil, &block)
|
737
|
+
offset = 0
|
738
|
+
positions.each_with_index do |p, i|
|
739
|
+
p = (p...p) if p.is_a?(Integer)
|
740
|
+
from = p.begin + offset
|
741
|
+
to = p.end + offset
|
742
|
+
p = p.exclude_end? ? (from...to) : (from..to)
|
743
|
+
# block returns the substitution, e.g.: { |text, pos| text[pos].upcase }
|
744
|
+
r = replacements ? replacements[i] : block.call(text[p], p, text)
|
745
|
+
text[p] = r
|
746
|
+
# add the change in length to offset
|
747
|
+
offset += r.size - (p.end - p.begin + (p.exclude_end? ? 0 : 1))
|
748
|
+
end
|
749
|
+
text
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|