Last active
August 29, 2015 13:59
-
-
Save emphaticsunshine/10959128 to your computer and use it in GitHub Desktop.
SASS forking to override paths
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@path '("bootstrap": "../components/bootstrap-sass/vendor/assets/stylesheets/bootstrap")'; | |
@import "bootstrap"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#sass{$version}/lib/sass/tree/import_node.rb | |
module Sass | |
module Tree | |
# A static node that wraps the {Sass::Tree} for an `@import`ed file. | |
# It doesn't have a functional purpose other than to add the `@import`ed file | |
# to the backtrace if an error occurs. | |
class ImportNode < RootNode | |
# The name of the imported file as it appears in the Sass document. | |
# | |
# @return [String] | |
attr_reader :imported_filename | |
# Sets the imported file. | |
attr_writer :imported_file | |
attr_accessor :immutable_path | |
# @param imported_filename [String] The name of the imported file | |
def initialize(imported_filename) | |
@imported_filename = imported_filename | |
super(nil) | |
end | |
def invisible?; to_s.empty?; end | |
# Returns the imported file. | |
# | |
# @return [Sass::Engine] | |
# @raise [Sass::SyntaxError] If no file could be found to import. | |
def imported_file | |
@imported_file ||= import | |
end | |
# Returns whether or not this import should emit a CSS @import declaration | |
# | |
# @return [Boolean] Whether or not this is a simple CSS @import declaration. | |
def css_import?(immutable_path = {}) | |
self.immutable_path = immutable_path | |
self.immutable_path = {} if self.immutable_path.nil? | |
if @imported_filename =~ /\.css$/ | |
@imported_filename | |
elsif imported_file.is_a?(String) && imported_file =~ /\.css$/ | |
imported_file | |
end | |
end | |
private | |
def import | |
paths = @options[:load_paths] | |
if @options[:importer] | |
if(self.immutable_path[imported_filename]) | |
@imported_filename = self.immutable_path[imported_filename] | |
end | |
f = @options[:importer].find_relative( | |
@imported_filename, @options[:filename], options_for_importer) | |
return f if f | |
end | |
paths.each do |p| | |
f = p.find(@imported_filename, options_for_importer) | |
return f if f | |
end | |
message = "File to import not found or unreadable: #{@imported_filename}.\n" | |
if paths.size == 1 | |
message << "Load path: #{paths.first}" | |
else | |
message << "Load paths:\n " << paths.join("\n ") | |
end | |
raise SyntaxError.new(message) | |
rescue SyntaxError => e | |
raise SyntaxError.new(e.message, :line => line, :filename => @filename) | |
end | |
def options_for_importer | |
@options.merge(:_from_import_node => true) | |
end | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#sass{$version}/lib/sass/tree/visitors/perform.rb | |
require "json" | |
# A visitor for converting a dynamic Sass tree into a static Sass tree. | |
class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base | |
attr_accessor :immutable_path | |
class << self | |
# @param root [Tree::Node] The root node of the tree to visit. | |
# @param environment [Sass::Environment] The lexical environment. | |
# @return [Tree::Node] The resulting tree of static nodes. | |
def visit(root, environment = nil) | |
new(environment).send(:visit, root) | |
end | |
# @api private | |
# @comment | |
# rubocop:disable MethodLength | |
def perform_arguments(callable, args, splat) | |
desc = "#{callable.type.capitalize} #{callable.name}" | |
downcase_desc = "#{callable.type} #{callable.name}" | |
# All keywords are contained in splat.keywords for consistency, | |
# even if there were no splats passed in. | |
old_keywords_accessed = splat.keywords_accessed | |
keywords = splat.keywords | |
splat.keywords_accessed = old_keywords_accessed | |
begin | |
unless keywords.empty? | |
unknown_args = Sass::Util.array_minus(keywords.keys, | |
callable.args.map {|var| var.first.underscored_name}) | |
if callable.splat && unknown_args.include?(callable.splat.underscored_name) | |
raise Sass::SyntaxError.new("Argument $#{callable.splat.name} of #{downcase_desc} " + | |
"cannot be used as a named argument.") | |
elsif unknown_args.any? | |
description = unknown_args.length > 1 ? 'the following arguments:' : 'an argument named' | |
raise Sass::SyntaxError.new("#{desc} doesn't have #{description} " + | |
"#{unknown_args.map {|name| "$#{name}"}.join ', '}.") | |
end | |
end | |
rescue Sass::SyntaxError => keyword_exception | |
end | |
# If there's no splat, raise the keyword exception immediately. The actual | |
# raising happens in the ensure clause at the end of this function. | |
return if keyword_exception && !callable.splat | |
if args.size > callable.args.size && !callable.splat | |
takes = callable.args.size | |
passed = args.size | |
raise Sass::SyntaxError.new( | |
"#{desc} takes #{takes} argument#{'s' unless takes == 1} " + | |
"but #{passed} #{passed == 1 ? 'was' : 'were'} passed.") | |
end | |
splat_sep = :comma | |
if splat | |
args += splat.to_a | |
splat_sep = splat.separator | |
end | |
env = Sass::Environment.new(callable.environment) | |
callable.args.zip(args[0...callable.args.length]) do |(var, default), value| | |
if value && keywords.has_key?(var.name) | |
raise Sass::SyntaxError.new("#{desc} was passed argument $#{var.name} " + | |
"both by position and by name.") | |
end | |
value ||= keywords.delete(var.name) | |
value ||= default && default.perform(env) | |
raise Sass::SyntaxError.new("#{desc} is missing argument #{var.inspect}.") unless value | |
env.set_local_var(var.name, value) | |
end | |
if callable.splat | |
rest = args[callable.args.length..-1] || [] | |
arg_list = Sass::Script::Value::ArgList.new(rest, keywords, splat_sep) | |
arg_list.options = env.options | |
env.set_local_var(callable.splat.name, arg_list) | |
end | |
yield env | |
rescue StandardError => e | |
ensure | |
# If there's a keyword exception, we don't want to throw it immediately, | |
# because the invalid keywords may be part of a glob argument that should be | |
# passed on to another function. So we only raise it if we reach the end of | |
# this function *and* the keywords attached to the argument list glob object | |
# haven't been accessed. | |
# | |
# The keyword exception takes precedence over any Sass errors, but not over | |
# non-Sass exceptions. | |
if keyword_exception && | |
!(arg_list && arg_list.keywords_accessed) && | |
(e.nil? || e.is_a?(Sass::SyntaxError)) | |
raise keyword_exception | |
elsif e | |
raise e | |
end | |
end | |
# @api private | |
# @return [Sass::Script::Value::ArgList] | |
def perform_splat(splat, performed_keywords, kwarg_splat, environment) | |
args, kwargs, separator = [], nil, :comma | |
if splat | |
splat = splat.perform(environment) | |
separator = splat.separator || separator | |
if splat.is_a?(Sass::Script::Value::ArgList) | |
args = splat.to_a | |
kwargs = splat.keywords | |
elsif splat.is_a?(Sass::Script::Value::Map) | |
kwargs = arg_hash(splat) | |
else | |
args = splat.to_a | |
end | |
end | |
kwargs ||= Sass::Util::NormalizedMap.new | |
kwargs.update(performed_keywords) | |
if kwarg_splat | |
kwarg_splat = kwarg_splat.perform(environment) | |
unless kwarg_splat.is_a?(Sass::Script::Value::Map) | |
raise Sass::SyntaxError.new("Variable keyword arguments must be a map " + | |
"(was #{kwarg_splat.inspect}).") | |
end | |
kwargs.update(arg_hash(kwarg_splat)) | |
end | |
Sass::Script::Value::ArgList.new(args, kwargs, separator) | |
end | |
private | |
def arg_hash(map) | |
Sass::Util.map_keys(map.to_h) do |key| | |
next key.value if key.is_a?(Sass::Script::Value::String) | |
raise Sass::SyntaxError.new("Variable keyword argument map must have string keys.\n" + | |
"#{key.inspect} is not a string in #{map.inspect}.") | |
end | |
end | |
end | |
# @comment | |
# rubocop:enable MethodLength | |
protected | |
def initialize(env) | |
@environment = env | |
end | |
# If an exception is raised, this adds proper metadata to the backtrace. | |
def visit(node) | |
return super(node.dup) unless @environment | |
@environment.stack.with_base(node.filename, node.line) {super(node.dup)} | |
rescue Sass::SyntaxError => e | |
e.modify_backtrace(:filename => node.filename, :line => node.line) | |
raise e | |
end | |
# Keeps track of the current environment. | |
def visit_children(parent) | |
with_environment Sass::Environment.new(@environment, parent.options) do | |
parent.children = super.flatten | |
parent | |
end | |
end | |
# Runs a block of code with the current environment replaced with the given one. | |
# | |
# @param env [Sass::Environment] The new environment for the duration of the block. | |
# @yield A block in which the environment is set to `env`. | |
# @return [Object] The return value of the block. | |
def with_environment(env) | |
old_env, @environment = @environment, env | |
yield | |
ensure | |
@environment = old_env | |
end | |
# Sets the options on the environment if this is the top-level root. | |
def visit_root(node) | |
yield | |
rescue Sass::SyntaxError => e | |
e.sass_template ||= node.template | |
raise e | |
end | |
# Removes this node from the tree if it's a silent comment. | |
def visit_comment(node) | |
return [] if node.invisible? | |
node.resolved_value = run_interp_no_strip(node.value) | |
node.resolved_value.gsub!(/\\([\\#])/, '\1') | |
node | |
end | |
# Prints the expression to STDERR. | |
def visit_debug(node) | |
res = node.expr.perform(@environment) | |
if res.is_a?(Sass::Script::Value::String) | |
res = res.value | |
else | |
res = res.to_sass | |
end | |
if node.filename | |
Sass::Util.sass_warn "#{node.filename}:#{node.line} DEBUG: #{res}" | |
else | |
Sass::Util.sass_warn "Line #{node.line} DEBUG: #{res}" | |
end | |
[] | |
end | |
# Runs the child nodes once for each value in the list. | |
def visit_each(node) | |
list = node.list.perform(@environment) | |
with_environment Sass::Environment.new(@environment) do | |
list.to_a.map do |value| | |
if node.vars.length == 1 | |
@environment.set_local_var(node.vars.first, value) | |
else | |
node.vars.zip(value.to_a) do |(var, sub_value)| | |
@environment.set_local_var(var, sub_value || Sass::Script::Value::Null.new) | |
end | |
end | |
node.children.map {|c| visit(c)} | |
end.flatten | |
end | |
end | |
# Runs SassScript interpolation in the selector, | |
# and then parses the result into a {Sass::Selector::CommaSequence}. | |
def visit_extend(node) | |
parser = Sass::SCSS::StaticParser.new(run_interp(node.selector), | |
node.filename, node.options[:importer], node.line) | |
node.resolved_selector = parser.parse_selector | |
node | |
end | |
# Runs the child nodes once for each time through the loop, varying the variable each time. | |
def visit_for(node) | |
from = node.from.perform(@environment) | |
to = node.to.perform(@environment) | |
from.assert_int! | |
to.assert_int! | |
to = to.coerce(from.numerator_units, from.denominator_units) | |
direction = from.to_i > to.to_i ? -1 : 1 | |
range = Range.new(direction * from.to_i, direction * to.to_i, node.exclusive) | |
with_environment Sass::Environment.new(@environment) do | |
range.map do |i| | |
@environment.set_local_var(node.var, | |
Sass::Script::Value::Number.new(direction * i, | |
from.numerator_units, from.denominator_units)) | |
node.children.map {|c| visit(c)} | |
end.flatten | |
end | |
end | |
# Loads the function into the environment. | |
def visit_function(node) | |
env = Sass::Environment.new(@environment, node.options) | |
@environment.set_local_function(node.name, | |
Sass::Callable.new(node.name, node.args, node.splat, env, | |
node.children, !:has_content, "function")) | |
[] | |
end | |
# Runs the child nodes if the conditional expression is true; | |
# otherwise, tries the else nodes. | |
def visit_if(node) | |
if node.expr.nil? || node.expr.perform(@environment).to_bool | |
yield | |
node.children | |
elsif node.else | |
visit(node.else) | |
else | |
[] | |
end | |
end | |
# Returns a static DirectiveNode if this is importing a CSS file, | |
# or parses and includes the imported Sass file. | |
def visit_import(node) | |
if (path = node.css_import?(@immutable_path)) | |
resolved_node = Sass::Tree::CssImportNode.resolved("url(#{path})") | |
resolved_node.source_range = node.source_range | |
return resolved_node | |
end | |
file = node.imported_file | |
if @environment.stack.frames.any? {|f| f.is_import? && f.filename == file.options[:filename]} | |
handle_import_loop!(node) | |
end | |
begin | |
@environment.stack.with_import(node.filename, node.line) do | |
root = file.to_tree | |
Sass::Tree::Visitors::CheckNesting.visit(root) | |
node.children = root.children.map {|c| visit(c)}.flatten | |
node | |
end | |
rescue Sass::SyntaxError => e | |
e.modify_backtrace(:filename => node.imported_file.options[:filename]) | |
e.add_backtrace(:filename => node.filename, :line => node.line) | |
raise e | |
end | |
end | |
# Loads a mixin into the environment. | |
def visit_mixindef(node) | |
env = Sass::Environment.new(@environment, node.options) | |
@environment.set_local_mixin(node.name, | |
Sass::Callable.new(node.name, node.args, node.splat, env, | |
node.children, node.has_content, "mixin")) | |
[] | |
end | |
# Runs a mixin. | |
def visit_mixin(node) | |
@environment.stack.with_mixin(node.filename, node.line, node.name) do | |
mixin = @environment.mixin(node.name) | |
raise Sass::SyntaxError.new("Undefined mixin '#{node.name}'.") unless mixin | |
if node.children.any? && !mixin.has_content | |
raise Sass::SyntaxError.new(%Q{Mixin "#{node.name}" does not accept a content block.}) | |
end | |
args = node.args.map {|a| a.perform(@environment)} | |
keywords = Sass::Util.map_vals(node.keywords) {|v| v.perform(@environment)} | |
splat = self.class.perform_splat(node.splat, keywords, node.kwarg_splat, @environment) | |
self.class.perform_arguments(mixin, args, splat) do |env| | |
env.caller = Sass::Environment.new(@environment) | |
env.content = [node.children, @environment] if node.has_children | |
trace_node = Sass::Tree::TraceNode.from_node(node.name, node) | |
with_environment(env) {trace_node.children = mixin.tree.map {|c| visit(c)}.flatten} | |
trace_node | |
end | |
end | |
rescue Sass::SyntaxError => e | |
e.modify_backtrace(:mixin => node.name, :line => node.line) | |
e.add_backtrace(:line => node.line) | |
raise e | |
end | |
def visit_content(node) | |
content, content_env = @environment.content | |
return [] unless content | |
@environment.stack.with_mixin(node.filename, node.line, '@content') do | |
trace_node = Sass::Tree::TraceNode.from_node('@content', node) | |
content_env = Sass::Environment.new(content_env) | |
content_env.caller = Sass::Environment.new(@environment) | |
with_environment(content_env) do | |
trace_node.children = content.map {|c| visit(c.dup)}.flatten | |
end | |
trace_node | |
end | |
rescue Sass::SyntaxError => e | |
e.modify_backtrace(:mixin => '@content', :line => node.line) | |
e.add_backtrace(:line => node.line) | |
raise e | |
end | |
# Runs any SassScript that may be embedded in a property. | |
def visit_prop(node) | |
node.resolved_name = run_interp(node.name) | |
val = node.value.perform(@environment) | |
node.resolved_value = val.to_s | |
node.value_source_range = val.source_range if val.source_range | |
yield | |
end | |
# Returns the value of the expression. | |
def visit_return(node) | |
throw :_sass_return, node.expr.perform(@environment) | |
end | |
# Runs SassScript interpolation in the selector, | |
# and then parses the result into a {Sass::Selector::CommaSequence}. | |
def visit_rule(node) | |
old_at_root_without_rule, @at_root_without_rule = @at_root_without_rule, false | |
parser = Sass::SCSS::StaticParser.new(run_interp(node.rule), | |
node.filename, node.options[:importer], node.line) | |
node.parsed_rules ||= parser.parse_selector | |
node.resolved_rules = node.parsed_rules.resolve_parent_refs( | |
@environment.selector, !old_at_root_without_rule) | |
node.stack_trace = @environment.stack.to_s if node.options[:trace_selectors] | |
with_environment Sass::Environment.new(@environment, node.options) do | |
@environment.selector = node.resolved_rules | |
node.children = node.children.map {|c| visit(c)}.flatten | |
end | |
node | |
ensure | |
@at_root_without_rule = old_at_root_without_rule | |
end | |
# Sets a variable that indicates that the first level of rule nodes | |
# shouldn't include the parent selector by default. | |
def visit_atroot(node) | |
if node.query | |
parser = Sass::SCSS::StaticParser.new(run_interp(node.query), | |
node.filename, node.options[:importer], node.line) | |
node.resolved_type, node.resolved_value = parser.parse_static_at_root_query | |
else | |
node.resolved_type, node.resolved_value = :without, ['rule'] | |
end | |
old_at_root_without_rule = @at_root_without_rule | |
@at_root_without_rule = true if node.exclude?('rule') | |
yield | |
ensure | |
@at_root_without_rule = old_at_root_without_rule | |
end | |
# Loads the new variable value into the environment. | |
def visit_variable(node) | |
env = @environment | |
identifier = [node.name, node.filename, node.line] | |
if node.global | |
env = env.global_env | |
elsif env.parent && env.is_var_global?(node.name) && | |
!env.global_env.global_warning_given.include?(identifier) | |
env.global_env.global_warning_given.add(identifier) | |
var_expr = "$#{node.name}: #{node.expr.to_sass(env.options)} !global" | |
var_expr << " !default" if node.guarded | |
location = "on line #{node.line}" | |
location << " of #{node.filename}" if node.filename | |
Sass::Util.sass_warn <<WARNING | |
DEPRECATION WARNING #{location}: | |
Assigning to global variable "$#{node.name}" by default is deprecated. | |
In future versions of Sass, this will create a new local variable. | |
If you want to assign to the global variable, use "#{var_expr}" instead. | |
Note that this will be incompatible with Sass 3.2. | |
WARNING | |
end | |
var = env.var(node.name) | |
return [] if node.guarded && var && !var.null? | |
val = node.expr.perform(@environment) | |
if node.expr.source_range | |
val.source_range = node.expr.source_range | |
else | |
val.source_range = node.source_range | |
end | |
env.set_var(node.name, val) | |
[] | |
end | |
# Prints the expression to STDERR with a stylesheet trace. | |
def visit_warn(node) | |
res = node.expr.perform(@environment) | |
res = res.value if res.is_a?(Sass::Script::Value::String) | |
msg = "WARNING: #{res}\n " | |
msg << @environment.stack.to_s.gsub("\n", "\n ") << "\n" | |
Sass::Util.sass_warn msg | |
[] | |
end | |
# Runs the child nodes until the continuation expression becomes false. | |
def visit_while(node) | |
children = [] | |
with_environment Sass::Environment.new(@environment) do | |
children += node.children.map {|c| visit(c)} while node.expr.perform(@environment).to_bool | |
end | |
children.flatten | |
end | |
def visit_directive(node) | |
node.resolved_value = run_interp(node.value) | |
with_environment Sass::Environment.new(@environment) do | |
node.children = node.children.map {|c| visit(c)}.flatten | |
node | |
end | |
end | |
def visit_media(node) | |
parser = Sass::SCSS::StaticParser.new(run_interp(node.query), | |
node.filename, node.options[:importer], node.line) | |
node.resolved_query ||= parser.parse_media_query_list | |
yield | |
end | |
def visit_supports(node) | |
node.condition = node.condition.deep_copy | |
node.condition.perform(@environment) | |
yield | |
end | |
def visit_cssimport(node) | |
node.resolved_uri = run_interp([node.uri]) | |
if node.query | |
parser = Sass::SCSS::StaticParser.new(run_interp(node.query), | |
node.filename, node.options[:importer], node.line) | |
node.resolved_query ||= parser.parse_media_query_list | |
end | |
yield | |
end | |
private | |
def run_interp_no_strip(text) | |
text.map do |r| | |
next r if r.is_a?(String) | |
val = r.perform(@environment) | |
# Interpolated strings should never render with quotes | |
next val.value if val.is_a?(Sass::Script::Value::String) | |
val.to_s | |
end.join | |
end | |
def run_interp(text) | |
run_interp_no_strip(text).strip | |
end | |
def handle_import_loop!(node) | |
msg = "An @import loop has been found:" | |
files = @environment.stack.frames.select {|f| f.is_import?}.map {|f| f.filename}.compact | |
if node.filename == node.imported_file.options[:filename] | |
raise Sass::SyntaxError.new("#{msg} #{node.filename} imports itself") | |
end | |
files << node.filename << node.imported_file.options[:filename] | |
msg << "\n" << Sass::Util.enum_cons(files, 2).map do |m1, m2| | |
" #{m1} imports #{m2}" | |
end.join("\n") | |
raise Sass::SyntaxError.new(msg) | |
end | |
def visit_path(node) | |
@immutable_path = {} if @immutable_path.nil? | |
get_path(node) | |
end | |
def get_path(node) | |
raw_path = run_interp(node.value) | |
cleaned_path = clean_path(raw_path) | |
puts cleaned_path | |
cleaned_path = JSON.parse(cleaned_path); | |
cleaned_path.each do |old_path, new_path| | |
@immutable_path[old_path] = new_path | |
end | |
end | |
def clean_path(path) | |
parsed_path = path[6..-1] | |
parsed_path.gsub!(/\A'|'\Z/, '') | |
parsed_path.gsub!("(", "{") | |
parsed_path.gsub!(")", "}") | |
return parsed_path | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment