Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Source range computation implementation for the SASS parser #569

Merged
merged 2 commits into from

6 participants

@crdev

NB: This branch is based off of sourcemap_fix_relpath, otherwise it would not make any sense.

Sourcemaps can now be constructed for nodes built while parsing SASS (engine.rb). A few fixes to the existing code have been made. All SCSS sourcemap tests have received their SASS counterparts.

Alexander Pa... added some commits
Alexander Pavlov Correctly compute relative_sourcemap_path and pass it as string, as e…
…xpected by render_with_sourcemap
d2aad70
Alexander Pavlov Implement source range tracking for some nodes when parsing SASS inpu…
…t, add SASS sourcemap tests, fix Line.offset computation
08e805c
@paulirish

:+1: this provides a LOT of power. can't wait to see it.

@Anahkiasen

What does it do exactly ?

@paulirish

Well the sourcemap can map all lines/columns from original sources into their destination CSS.

What this enables on the tool side: ctrl-click any property/value/selector and go to the location in the sass/scss where that originates: the expression, mixin, variable where it was birthed or computed. This makes the editing experience of working with sass and tweaking CSS much more enjoyable.

The UI side of the above is ready, and this PR enables the sourcemap that can deliver the mapping from sass to the tool.

Down the line I can imagine editing values on the right styles pane of Chrome DevTools and changing the original scss source.

@matthew-dean

Note that @paulirish says this feature is not SASS / SCSS specific and should work with any CSS pre-processing language. less/less.js#1038

@nex3 nex3 merged commit 08e805c into sass:sourcemap
@ruudk

When I run this command from my homedir (~):

sass --load-path ~/Downloads/ --scss --cache-location --sourcemap ~/Downloads/test.scss ~/Downloads/output.css

It creates the --sourcemap directory inside my homedirectory instead of the directory of ~/Downloads/output.css.

Is it possible to configure the location of the sourcemap?

@crdev

@ruudk: You did not specify the actual cache location after --cache-location, thus sass thinks that --sourcemap IS your desired --cache-location value.

@emagnier emagnier referenced this pull request in sass/libsass
Closed

Any plans for source-maps? [$150 awarded] #122

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 12, 2012
  1. Correctly compute relative_sourcemap_path and pass it as string, as e…

    Alexander Pavlov authored
    …xpected by render_with_sourcemap
Commits on Nov 13, 2012
  1. Implement source range tracking for some nodes when parsing SASS inpu…

    Alexander Pavlov authored
    …t, add SASS sourcemap tests, fix Line.offset computation
This page is out of date. Refresh to see the latest.
View
120 lib/sass/engine.rb
@@ -452,7 +452,7 @@ def tabulate(string)
raise SyntaxError.new(message, :line => index)
end
- lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
+ lines << Line.new(line.strip, line_tabs, index, line_tab_str.size, @options[:filename], [])
end
lines
end
@@ -496,6 +496,7 @@ def tree(arr, i = 0)
def build_tree(parent, line, root = false)
@line = line.index
+ @offset = line.offset
node_or_nodes = parse_line(parent, line, root)
Array(node_or_nodes).each do |node|
@@ -584,7 +585,12 @@ def parse_line(parent, line, root)
name, value = line.text.scan(PROPERTY_OLD)[0]
raise SyntaxError.new("Invalid property: \"#{line.text}\".",
:line => @line) if name.nil? || value.nil?
- parse_property(name, parse_interp(name), value, :old, line)
+ name_end_offset = line.offset + 1 + name.length # 1 stands for the leading ':'.
+ if !value.empty?
+ index = line.text.index(value, name_end_offset)
+ name_end_offset = index if index
+ end
+ parse_property(name, parse_interp(name), value, :old, line, to_parser_offset(name_end_offset))
end
when ?$
parse_variable(line)
@@ -610,32 +616,51 @@ def parse_line(parent, line, root)
def parse_property_or_rule(line)
scanner = Sass::Util::MultibyteStringScanner.new(line.text)
hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
- parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
+ offset = line.offset
+ offset += hack_char.length if hack_char
+ parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line, to_parser_offset(offset))
unless res = parser.parse_interp_ident
- return Tree::RuleNode.new(parse_interp(line.text))
+ return Tree::RuleNode.new(parse_interp(line.text, line.offset))
end
+
+ ident_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
+ Sass::Source::Position.new(@line, parser.offset),
+ @options[:filename])
+ offset = parser.offset - 1
res.unshift(hack_char) if hack_char
if comment = scanner.scan(Sass::SCSS::RX::COMMENT)
res << comment
+ offset += comment.length
end
name = line.text[0...scanner.pos]
- if scanner.scan(/\s*:(?:\s|$)/)
- parse_property(name, res, scanner.rest, :new, line)
+ if (scanned = scanner.scan(/\s*:(?:\s|$)/))
+ offset += scanned.length
+ property = parse_property(name, res, scanner.rest, :new, line, offset)
+ property.name_source_range = ident_range
+ property
else
res.pop if comment
Tree::RuleNode.new(res + parse_interp(scanner.rest))
end
end
- def parse_property(name, parsed_name, value, prop, line)
+ def parse_property(name, parsed_name, value, prop, line, offset)
+ start_offset = offset
if value.strip.empty?
expr = Sass::Script::String.new("")
+ end_offset = start_offset
else
- expr = parse_script(value, :offset => line.offset + line.text.index(value))
+ expr = parse_script(value, :offset => to_parser_offset(offset))
+ end_offset = expr.options[:end_offset]
end
node = Tree::PropNode.new(parse_interp(name), expr, prop)
+ node.value_source_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(line.index, to_parser_offset(start_offset)),
+ Sass::Source::Position.new(line.index, end_offset),
+ @options[:filename])
if value.strip.empty? && line.children.empty?
raise SyntaxError.new(
"Invalid property: \"#{node.declaration}\" (no value)." +
@@ -652,7 +677,12 @@ def parse_variable(line)
raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
:line => @line) unless name && value
- expr = parse_script(value, :offset => line.offset + line.text.index(value))
+ # This workaround is needed for the case when the variable value is part of the identifier,
+ # otherwise we end up with the offset equal to the value index inside the name:
+ # $red_color: red;
+ var_lhs_length = 1 + name.length # 1 stands for '$'
+ index = line.text.index(value, line.offset + var_lhs_length) || 0
+ expr = parse_script(value, :offset => to_parser_offset(line.offset + index))
Tree::VariableNode.new(name, expr, default)
end
@@ -664,7 +694,8 @@ def parse_comment(line)
if silent
value = [line.text]
else
- value = self.class.parse_interp(line.text, line.index, line.offset, :filename => @filename)
+ value = self.class.parse_interp(
+ line.text, line.index, to_parser_offset(line.offset), :filename => @filename)
value[0].slice!(2) if loud # get rid of the "!"
end
value = with_extracted_values(value) do |str|
@@ -739,7 +770,7 @@ def parse_directive(parent, line, root)
:line => @line + 1) unless line.children.empty?
Tree::CharsetNode.new(name)
when 'media'
- parser = Sass::SCSS::Parser.new(value, @options[:filename], @line)
+ parser = Sass::SCSS::Parser.new(value, @options[:filename], @line, to_parser_offset(@offset))
Tree::MediaNode.new(parser.parse_media_query_list.to_a)
else
Tree::DirectiveNode.new(
@@ -830,29 +861,56 @@ def parse_import(line, value, offset)
def parse_import_arg(scanner, offset)
return if scanner.eos?
- if scanner.match?(/url\(/i)
- script_parser = Sass::Script::Parser.new(scanner, @line, offset, @options)
+ if (match_length = scanner.match?(/url\(/i))
+ offset += match_length
+ script_parser = Sass::Script::Parser.new(scanner, @line, to_parser_offset(offset), @options)
str = script_parser.parse_string
- media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
+ parser_offset = str.source_range.end_pos.offset if str.source_range
+ media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line, parser_offset)
media = media_parser.parse_media_query_list
- return Tree::CssImportNode.new(str, media.to_a)
+ node = Tree::CssImportNode.new(str, media.to_a)
+ node.source_range = Sass::Source::Range.new(str.source_range.start_pos, media_parser.offset, @options[:filename])
+ return node
end
unless str = scanner.scan(Sass::SCSS::RX::STRING)
- return Tree::ImportNode.new(scanner.scan(/[^,;]+/))
- end
-
+ scanned = scanner.scan(/[^,;]+/)
+ node = Tree::ImportNode.new(scanned)
+ start_parser_offset = to_parser_offset(offset)
+ node.source_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(@line, start_parser_offset),
+ Sass::Source::Position.new(@line, start_parser_offset + scanned.length),
+ @options[:filename])
+ return node
+ end
+
+ start_offset = offset
+ offset += str.length
val = scanner[1] || scanner[2]
- scanner.scan(/\s*/)
+ scanned = scanner.scan(/\s*/)
if !scanner.match?(/[,;]|$/)
- media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line)
+ offset += scanned.length if scanned
+ media_parser = Sass::SCSS::Parser.new(scanner, @options[:filename], @line, offset)
media = media_parser.parse_media_query_list
- Tree::CssImportNode.new(str || uri, media.to_a)
+ node = Tree::CssImportNode.new(str || uri, media.to_a)
+ node.source_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
+ Sass::Source::Position.new(@line, media_parser.offset),
+ @options[:filename])
elsif val =~ /^(https?:)?\/\//
- Tree::CssImportNode.new("url(#{val})")
+ node = Tree::CssImportNode.new("url(#{val})")
+ node.source_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
+ @options[:filename])
else
- Tree::ImportNode.new(val)
+ node = Tree::ImportNode.new(val)
+ node.source_range = Sass::Source::Range.new(
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
+ @options[:filename])
end
+ node
end
MIXIN_DEF_RE = /^(?:=|@mixin)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
@@ -861,7 +919,7 @@ def parse_mixin_definition(line)
raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
offset = line.offset + line.text.size - arg_string.size
- args, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
parse_mixin_definition_arglist
Tree::MixinDefNode.new(name, args, splat)
end
@@ -881,7 +939,7 @@ def parse_mixin_include(line, root)
raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
offset = line.offset + line.text.size - arg_string.size
- args, keywords, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
+ args, keywords, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
parse_mixin_include_arglist
Tree::MixinNode.new(name, args, keywords, splat)
end
@@ -892,14 +950,14 @@ def parse_function(line, root)
raise SyntaxError.new("Invalid function definition \"#{line.text}\".") if name.nil?
offset = line.offset + line.text.size - arg_string.size
- args, splat = Script::Parser.new(arg_string.strip, @line, offset, @options).
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
parse_function_definition_arglist
Tree::FunctionNode.new(name, args, splat)
end
def parse_script(script, options = {})
line = options[:line] || @line
- offset = options[:offset] || 0
+ offset = options[:offset] || @offset + 1
Script.parse(script, line, offset, @options)
end
@@ -927,6 +985,11 @@ def parse_interp(text, offset = 0)
self.class.parse_interp(text, @line, offset, :filename => @filename)
end
+ # Parser tracks 1-based line and offset, so our offset should be converted.
+ def to_parser_offset(offset)
+ offset + 1
+ end
+
# It's important that this have strings (at least)
# at the beginning, the end, and between each Script::Node.
#
@@ -940,8 +1003,9 @@ def self.parse_interp(text, line, offset, options)
res << "\\" * (escapes - 1) << '#{'
else
res << "\\" * [0, escapes - 1].max
+ # Add 1 to emulate to_parser_offset.
res << Script::Parser.new(
- scan, line, offset + scan.pos - scan.matched_size, options).
+ scan, line, offset + scan.pos - scan.matched_size + 1, options).
parse_interpolated
end
end
View
4 lib/sass/exec.rb
@@ -338,8 +338,8 @@ def process_result
if sourcemap.is_a? File
relative_sourcemap_path = Pathname.new(@options[:sourcemap_filename]).
- relative_path_from(Pathname.new(@options[:output_filename]))
- rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path)
+ relative_path_from(Pathname.new(@options[:output_filename]).dirname)
+ rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path.to_s)
output.write(rendered)
sourcemap.puts(mapping.to_json(@options[:output_filename]))
else
View
14 lib/sass/script/parser.rb
@@ -39,9 +39,11 @@ def initialize(str, line, offset, options = {})
# @return [Script::Node] The root node of the parse tree
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
def parse_interpolated
+ start_pos = source_position
expr = assert_expr :expr
assert_tok :end_interpolation
expr.options = @options
+ expr.source_range = range(start_pos)
expr
rescue Sass::SyntaxError => e
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -56,6 +58,7 @@ def parse
expr = assert_expr :expr
assert_done
expr.options = @options
+ expr.options[:end_offset] = offset
expr
rescue Sass::SyntaxError => e
e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -250,6 +253,10 @@ def source_position
Sass::Source::Position.new(line, offset)
end
+ def token_start_position(token)
+ Sass::Source::Position.new(token.line, token.offset)
+ end
+
def range(start_pos, end_pos=source_position)
Sass::Source::Range.new(start_pos, end_pos, @options[:filename])
end
@@ -338,19 +345,18 @@ def ident
return funcall unless @lexer.peek && @lexer.peek.type == :ident
return if @stop_at && @stop_at.include?(@lexer.peek.value)
- start_pos = source_position
name = @lexer.next
if color = Color::COLOR_NAMES[name.value.downcase]
- return node(Color.new(color), start_pos)
+ return node(Color.new(color), token_start_position(name), source_position)
end
- node(Script::String.new(name.value, :identifier), start_pos)
+ node(Script::String.new(name.value, :identifier), token_start_position(name), source_position)
end
def funcall
return raw unless tok = try_tok(:funcall)
args, keywords, splat = fn_arglist || [[], {}]
assert_tok(:rparen)
- node(Script::Funcall.new(tok.value, args, keywords, splat))
+ node(Script::Funcall.new(tok.value, args, keywords, splat), token_start_position(tok), source_position)
end
def defn_arglist!(must_have_parens)
View
4 lib/sass/scss/parser.rb
@@ -5,6 +5,10 @@ module SCSS
# The parser for SCSS.
# It parses a string of code into a tree of {Sass::Tree::Node}s.
class Parser
+
+ # Expose for the SASS parser.
+ attr_accessor :offset
+
# @param str [String, StringScanner] The source document to parse.
# Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
# for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
View
4 lib/sass/tree/visitors/perform.rb
@@ -213,7 +213,9 @@ def visit_if(node)
# or parses and includes the imported Sass file.
def visit_import(node)
if path = node.css_import?
- return Sass::Tree::CssImportNode.resolved("url(#{path})")
+ resolved_node = Sass::Tree::CssImportNode.resolved("url(#{path})")
+ resolved_node.source_range = node.source_range
+ return resolved_node
end
file = node.imported_file
handle_import_loop!(node) if @stack.any? {|e| e[:filename] == file.options[:filename]}
View
406 test/sass/source_map_test.rb
@@ -4,7 +4,7 @@
require File.dirname(__FILE__) + '/test_helper'
class SourcemapTest < Test::Unit::TestCase
- def test_simple_scss_mapping
+ def test_simple_mapping_scss
assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON
a {
foo: bar;
@@ -22,13 +22,36 @@ def test_simple_scss_mapping
{
"version": "3",
"mappings": ";EACE,GAAG,EAAE,GAAG;;EAER,SAAS,EAAE,IAAI",
-"sources": ["test_simple_scss_mapping_inline.scss"],
+"sources": ["test_simple_mapping_scss_inline.scss"],
"file": "test.css"
}
JSON
end
- def test_mapping_with_directory
+ def test_simple_mapping_sass
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, :syntax => :sass
+a
+ foo: bar
+ /* SOME COMMENT */
+ font-size: 12px
+SASS
+a {
+ foo: bar;
+ /* SOME COMMENT */
+ font-size: 12px; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+{
+"version": "3",
+"mappings": ";EACE,GAAG,EAAE,GAAG;;EAER,SAAS,EAAE,IAAI",
+"sources": ["test_simple_mapping_sass_inline.sass"],
+"file": "test.css"
+}
+JSON
+ end
+
+ def test_mapping_with_directory_scss
options = {:filename => "scss/style.scss", :output => "css/style.css"}
assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON, options
a {
@@ -53,8 +76,32 @@ def test_mapping_with_directory
JSON
end
+ def test_mapping_with_directory_sass
+ options = {:filename => "sass/style.sass", :output => "css/style.css", :syntax => :sass}
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, options
+a
+ foo: bar
+ /* SOME COMMENT */
+ font-size: 12px
+SASS
+a {
+ foo: bar;
+ /* SOME COMMENT */
+ font-size: 12px; }
+
+/*@ sourceMappingURL=style.css.map */
+CSS
+{
+"version": "3",
+"mappings": ";EACE,GAAG,EAAE,GAAG;;EAER,SAAS,EAAE,IAAI",
+"sources": ["..\\/sass\\/style.sass"],
+"file": "style.css"
+}
+JSON
+ end
+
unless Sass::Util.ruby1_8?
- def test_simple_charset_scss_mapping
+ def test_simple_charset_mapping_scss
assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON
a {
fóó: bár;
@@ -69,36 +116,77 @@ def test_simple_charset_scss_mapping
{
"version": "3",
"mappings": ";;EACE,GAAG,EAAE,GAAG",
-"sources": ["test_simple_charset_scss_mapping_inline.scss"],
+"sources": ["test_simple_charset_mapping_scss_inline.scss"],
"file": "test.css"
}
JSON
end
- def test_different_charset_than_encoding
- assert_parses_with_sourcemap(<<CSS.force_encoding("IBM866"), <<SASS.force_encoding("IBM866"), <<JSON)
+ def test_simple_charset_mapping_sass
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, :syntax => :sass
+a
+ fóó: bár
+SASS
+@charset "UTF-8";
+a {
+ fóó: bár; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+{
+"version": "3",
+"mappings": ";;EACE,GAAG,EAAE,GAAG",
+"sources": ["test_simple_charset_mapping_sass_inline.sass"],
+"file": "test.css"
+}
+JSON
+ end
+
+ def test_different_charset_than_encoding_scss
+ assert_parses_with_sourcemap(<<SCSS.force_encoding("IBM866"), <<CSS.force_encoding("IBM866"), <<JSON)
@charset "IBM866";
f\x86\x86 {
\x86: b;
}
-CSS
+SCSS
@charset "IBM866";
f\x86\x86 {
\x86: b; }
/*@ sourceMappingURL=test.css.map */
+CSS
+{
+"version": "3",
+"mappings": ";;EAEE,CAAC,EAAE,CAAC",
+"sources": ["test_different_charset_than_encoding_scss_inline.scss"],
+"file": "test.css"
+}
+JSON
+ end
+
+ def test_different_charset_than_encoding_sass
+ assert_parses_with_sourcemap(<<SASS.force_encoding("IBM866"), <<CSS.force_encoding("IBM866"), <<JSON, :syntax => :sass)
+@charset "IBM866"
+f\x86\x86
+ \x86: b
SASS
+@charset "IBM866";
+f\x86\x86 {
+ \x86: b; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
{
"version": "3",
"mappings": ";;EAEE,CAAC,EAAE,CAAC",
-"sources": ["test_different_charset_than_encoding_inline.scss"],
+"sources": ["test_different_charset_than_encoding_sass_inline.sass"],
"file": "test.css"
}
JSON
end
end
- def test_import_sourcemap
+ def test_import_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
@import {{1}}url(foo){{/1}},{{2}}url(moo) {{/2}}, {{3}}url(bar) {{/3}};
SCSS
@@ -110,35 +198,77 @@ def test_import_sourcemap
CSS
end
- def test_interpolation_and_vars_sourcemap
+ def test_import_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+@import {{1}}foo.css{{/1}}, {{2}}moo.css{{/2}}, {{3}}bar.css{{/3}}
+SASS
+{{1}}@import url(foo.css){{/1}};
+{{2}}@import url(moo.css){{/2}};
+{{3}}@import url(bar.css){{/3}};
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_interpolation_and_vars_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
$te: "te";
+$teal: {{4}}teal{{/4}};
p {
{{1}}con#{$te}nt{{/1}}: {{2}}"I a#{$te} #{5 + 10} pies!"{{/2}};
+ {{3}}color{{/3}}: $teal;
}
-
$name: foo;
$attr: border;
p.#{$name} {
- {{3}}#{$attr}-color{{/3}}: {{4}}blue{{/4}};
+ {{5}}#{$attr}-color{{/5}}: {{6}}blue{{/6}};
$font-size: 12px;
$line-height: 30px;
- {{5}}font{{/5}}: {{6}}#{$font-size}/#{$line-height}{{/6}};
+ {{7}}font{{/7}}: {{8}}#{$font-size}/#{$line-height}{{/8}};
}
SCSS
p {
- {{1}}content{{/1}}: {{2}}"I ate 15 pies!"{{/2}}; }
+ {{1}}content{{/1}}: {{2}}"I ate 15 pies!"{{/2}};
+ {{3}}color{{/3}}: {{4}}teal{{/4}}; }
p.foo {
- {{3}}border-color{{/3}}: {{4}}blue{{/4}};
- {{5}}font{{/5}}: {{6}}12px/30px{{/6}}; }
+ {{5}}border-color{{/5}}: {{6}}blue{{/6}};
+ {{7}}font{{/7}}: {{8}}12px/30px{{/8}}; }
/*@ sourceMappingURL=test.css.map */
CSS
end
- def test_selectors_properties_sourcemap
+ def test_interpolation_and_vars_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+$te: "te"
+$teal: {{4}}teal{{/4}}
+p
+ {{1}}con#{$te}nt{{/1}}: {{2}}"I a#{$te} #{5 + 10} pies!"{{/2}}
+ {{3}}color{{/3}}: $teal
+
+$name: foo
+$attr: border
+p.#{$name}
+ {{5}}#{$attr}-color{{/5}}: {{6}}blue{{/6}}
+ $font-size: 12px
+ $line-height: 30px
+ {{7}}font{{/7}}: {{8}}#{$font-size}/#{$line-height}{{/8}}
+SASS
+p {
+ {{1}}content{{/1}}: {{2}}"I ate 15 pies!"{{/2}};
+ {{3}}color{{/3}}: {{4}}teal{{/4}}; }
+
+p.foo {
+ {{5}}border-color{{/5}}: {{6}}blue{{/6}};
+ {{7}}font{{/7}}: {{8}}12px/30px{{/8}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_selectors_properties_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
$width: 2px;
$translucent-red: rgba(255, 0, 0, 0.5);
@@ -151,7 +281,7 @@ def test_selectors_properties_sourcemap
{{13}}color{{/13}}: {{14}}opacify($translucent-red, 0.3){{/14}};
}
&:after {
- {{15}}content{{/15}}: {{16}}"I ate #{5 + 10} pies thick!"{{/16}};
+ {{15}}content{{/15}}: {{16}}"I ate #{5 + 10} pies #{$width} thick!"{{/16}};
}
}
&:active {
@@ -177,7 +307,7 @@ def test_selectors_properties_sourcemap
{{11}}cursor{{/11}}: {{12}}e-resize{{/12}};
{{13}}color{{/13}}: {{14}}rgba(255, 0, 0, 0.8){{/14}}; }
a .special:after {
- {{15}}content{{/15}}: {{16}}"I ate 15 pies thick!"{{/16}}; }
+ {{15}}content{{/15}}: {{16}}"I ate 15 pies 2px thick!"{{/16}}; }
a:active {
{{17}}color{{/17}}: {{18}}#050709{{/18}};
{{19}}border{{/19}}: {{20}}2px solid black{{/20}}; }
@@ -186,7 +316,50 @@ def test_selectors_properties_sourcemap
CSS
end
- def test_extend_sourcemap
+ def test_selectors_properties_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+$width: 2px
+$translucent-red: rgba(255, 0, 0, 0.5)
+a
+ .special
+ {{7}}color{{/7}}: {{8}}red{{/8}}
+ &:hover
+ {{9}}foo{{/9}}: {{10}}bar{{/10}}
+ {{11}}cursor{{/11}}: {{12}}e + -resize{{/12}}
+ {{13}}color{{/13}}: {{14}}opacify($translucent-red, 0.3){{/14}}
+ &:after
+ {{15}}content{{/15}}: {{16}}"I ate #{5 + 10} pies #{$width} thick!"{{/16}}
+ &:active
+ {{17}}color{{/17}}: {{18}}#010203 + #040506{{/18}}
+ {{19}}border{{/19}}: {{20}}$width solid black{{/20}}
+
+ /* SOME COMMENT */
+ {{1}}font{{/1}}: {{2}}2px/3px{{/2}}
+ {{3}}family{{/3}}: {{4}}fantasy{{/4}}
+ {{5}}size{{/5}}: {{6}}1em + (2em * 3){{/6}}
+SASS
+a {
+ /* SOME COMMENT */
+ {{1}}font{{/1}}: {{2}}2px/3px{{/2}};
+ {{3}}font-family{{/3}}: {{4}}fantasy{{/4}};
+ {{5}}font-size{{/5}}: {{6}}7em{{/6}}; }
+ a .special {
+ {{7}}color{{/7}}: {{8}}red{{/8}}; }
+ a .special:hover {
+ {{9}}foo{{/9}}: {{10}}bar{{/10}};
+ {{11}}cursor{{/11}}: {{12}}e-resize{{/12}};
+ {{13}}color{{/13}}: {{14}}rgba(255, 0, 0, 0.8){{/14}}; }
+ a .special:after {
+ {{15}}content{{/15}}: {{16}}"I ate 15 pies 2px thick!"{{/16}}; }
+ a:active {
+ {{17}}color{{/17}}: {{18}}#050709{{/18}};
+ {{19}}border{{/19}}: {{20}}2px solid black{{/20}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_extend_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
.error {
{{1}}border{{/1}}: {{2}}1px #f00{{/2}};
@@ -208,7 +381,28 @@ def test_extend_sourcemap
CSS
end
- def test_for_sourcemap
+ def test_extend_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+.error
+ {{1}}border{{/1}}: {{2}}1px #f00{{/2}}
+ {{3}}background-color{{/3}}: {{4}}#fdd{{/4}}
+
+.seriousError
+ @extend .error
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}}
+SASS
+.error, .seriousError {
+ {{1}}border{{/1}}: {{2}}1px red{{/2}};
+ {{3}}background-color{{/3}}: {{4}}#ffdddd{{/4}}; }
+
+.seriousError {
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_for_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
@for $i from 1 through 3 {
.item-#{$i} { {{1}}width{{/1}}: {{2}}2em * $i{{/2}}; }
@@ -227,7 +421,26 @@ def test_for_sourcemap
CSS
end
- def test_while_sourcemap
+ def test_for_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+@for $i from 1 through 3
+ .item-#{$i}
+ {{1}}width{{/1}}: {{2}}2em * $i{{/2}}
+SASS
+.item-1 {
+ {{1}}width{{/1}}: {{2}}2em{{/2}}; }
+
+.item-2 {
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
+
+.item-3 {
+ {{1}}width{{/1}}: {{2}}6em{{/2}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_while_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
$i: 6;
@while $i > 0 {
@@ -248,7 +461,28 @@ def test_while_sourcemap
CSS
end
- def test_each_sourcemap
+def test_while_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+$i: 6
+@while $i > 0
+ .item-#{$i}
+ {{1}}width{{/1}}: {{2}}2em * $i{{/2}}
+ $i: $i - 2
+SASS
+.item-6 {
+ {{1}}width{{/1}}: {{2}}12em{{/2}}; }
+
+.item-4 {
+ {{1}}width{{/1}}: {{2}}8em{{/2}}; }
+
+.item-2 {
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_each_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
@each $animal in puma, sea-slug, egret, salamander {
.#{$animal}-icon {
@@ -272,7 +506,29 @@ def test_each_sourcemap
CSS
end
- def test_mixin_sourcemap
+ def test_each_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+@each $animal in puma, sea-slug, egret, salamander
+ .#{$animal}-icon
+ {{1}}background-image{{/1}}: {{2}}url('/images/#{$animal}.png'){{/2}}
+SASS
+.puma-icon {
+ {{1}}background-image{{/1}}: {{2}}url("/images/puma.png"){{/2}}; }
+
+.sea-slug-icon {
+ {{1}}background-image{{/1}}: {{2}}url("/images/sea-slug.png"){{/2}}; }
+
+.egret-icon {
+ {{1}}background-image{{/1}}: {{2}}url("/images/egret.png"){{/2}}; }
+
+.salamander-icon {
+ {{1}}background-image{{/1}}: {{2}}url("/images/salamander.png"){{/2}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
+ def test_mixin_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
@mixin large-text {
font: {
@@ -289,14 +545,14 @@ def test_mixin_sourcemap
@mixin dashed-border($color, $width: {{24}}1in{{/24}}) {
border: {
- {{9}}color{{/9}}: {{10}}$color{{/10}};
+ {{9}}color{{/9}}: $color;
{{11}}width{{/11}}: $width;
{{13}}style{{/13}}: {{14}}dashed{{/14}};
}
}
-p { @include dashed-border(blue); }
-h1 { @include dashed-border(blue, {{25}}2in{{/25}}); }
+p { @include dashed-border({{10}}blue{{/10}}); }
+h1 { @include dashed-border({{25}}blue{{/25}}, {{26}}2in{{/26}}); }
@mixin box-shadow($shadows...) {
{{18}}-moz-box-shadow{{/18}}: {{19}}$shadows{{/19}};
@@ -320,8 +576,8 @@ def test_mixin_sourcemap
{{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
h1 {
- {{9}}border-color{{/9}}: {{10}}blue{{/10}};
- {{11}}border-width{{/11}}: {{25}}2in{{/25}};
+ {{9}}border-color{{/9}}: {{25}}blue{{/25}};
+ {{11}}border-width{{/11}}: {{26}}2in{{/26}};
{{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
.shadows {
@@ -333,7 +589,64 @@ def test_mixin_sourcemap
CSS
end
- def test_function_sourcemap
+def test_mixin_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+=large-text
+ :font
+ {{1}}size{{/1}}: {{2}}20px{{/2}}
+ {{3}}weight{{/3}}: {{4}}bold{{/4}}
+ {{5}}color{{/5}}: {{6}}#ff0000{{/6}}
+
+.page-title
+ +large-text
+ {{7}}padding{{/7}}: {{8}}4px{{/8}}
+
+=dashed-border($color, $width: {{24}}1in{{/24}})
+ border:
+ {{9}}color{{/9}}: $color
+ {{11}}width{{/11}}: $width
+ {{13}}style{{/13}}: {{14}}dashed{{/14}}
+
+p
+ +dashed-border({{10}}blue{{/10}})
+
+h1
+ +dashed-border({{25}}blue{{/25}}, {{26}}2in{{/26}})
+
+=box-shadow($shadows...)
+ {{18}}-moz-box-shadow{{/18}}: {{19}}$shadows{{/19}}
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}$shadows{{/21}}
+ {{22}}box-shadow{{/22}}: {{23}}$shadows{{/23}}
+
+.shadows
+ +box-shadow(0px 4px 5px #666, 2px 6px 10px #999)
+SASS
+.page-title {
+ {{1}}font-size{{/1}}: {{2}}20px{{/2}};
+ {{3}}font-weight{{/3}}: {{4}}bold{{/4}};
+ {{5}}color{{/5}}: {{6}}red{{/6}};
+ {{7}}padding{{/7}}: {{8}}4px{{/8}}; }
+
+p {
+ {{9}}border-color{{/9}}: {{10}}blue{{/10}};
+ {{11}}border-width{{/11}}: {{24}}1in{{/24}};
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
+
+h1 {
+ {{9}}border-color{{/9}}: {{25}}blue{{/25}};
+ {{11}}border-width{{/11}}: {{26}}2in{{/26}};
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
+
+.shadows {
+ {{18}}-moz-box-shadow{{/18}}: {{19}}0px 4px 5px #666666, 2px 6px 10px #999999{{/19}};
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}0px 4px 5px #666666, 2px 6px 10px #999999{{/21}};
+ {{22}}box-shadow{{/22}}: {{23}}0px 4px 5px #666666, 2px 6px 10px #999999{{/23}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+end
+
+ def test_function_sourcemap_scss
assert_parses_with_mapping <<'SCSS', <<'CSS'
$grid-width: 20px;
$gutter-width: 5px;
@@ -350,6 +663,24 @@ def test_function_sourcemap
CSS
end
+ def test_function_sourcemap_sass
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
+$grid-width: 20px
+$gutter-width: 5px
+
+@function grid-width($n)
+ @return $n * $grid-width + ($n - 1) * $gutter-width
+
+sidebar
+ {{1}}width{{/1}}: {{2}}grid-width(5){{/2}}
+SASS
+sidebar {
+ {{1}}width{{/1}}: {{2}}120px{{/2}}; }
+
+/*@ sourceMappingURL=test.css.map */
+CSS
+ end
+
@private
ANNOTATION_REGEX = /\{\{(\/?)([^}]+)\}\}/
@@ -394,14 +725,15 @@ def build_mapping_from_annotations(scss, css, source_file_name)
map
end
- def assert_parses_with_mapping(scss, css, options={})
- scss_filename = filename_for_test(:scss)
- mapping = build_mapping_from_annotations(scss, css, scss_filename)
- scss.gsub!(ANNOTATION_REGEX, "")
+ def assert_parses_with_mapping(input, css, options={})
+ options[:syntax] ||= :scss
+ input_filename = filename_for_test(options[:syntax])
+ mapping = build_mapping_from_annotations(input, css, input_filename)
+ input.gsub!(ANNOTATION_REGEX, "")
css.gsub!(ANNOTATION_REGEX, "")
- rendered, sourcemap = render_with_sourcemap(scss, options)
+ rendered, sourcemap = render_with_sourcemap(input, options)
assert_equal css.rstrip, rendered.rstrip
- assert_sourcemaps_equal scss, css, mapping, sourcemap
+ assert_sourcemaps_equal input, css, mapping, sourcemap
end
def assert_positions_equal(expected, actual, lines, message = nil)
Something went wrong with that request. Please try again.