diff --git a/scss/compiler.py b/scss/compiler.py index 551f407d..9bc912d9 100644 --- a/scss/compiler.py +++ b/scss/compiler.py @@ -340,6 +340,7 @@ def parse_children(self, scope=None): for source_file in self.sources: rule = SassRule( source_file=source_file, + lineno=1, unparsed_contents=source_file.contents, namespace=root_namespace, @@ -1163,6 +1164,7 @@ def _nest_at_rules(self, rule, scope, block): source_file=rule.source_file, import_key=rule.import_key, lineno=block.lineno, + num_header_lines=block.header.num_lines, unparsed_contents=block.unparsed_contents, legacy_compiler_options=rule.legacy_compiler_options, @@ -1204,6 +1206,7 @@ def _nest_rules(self, rule, scope, block): source_file=rule.source_file, import_key=rule.import_key, lineno=block.lineno, + num_header_lines=block.header.num_lines, unparsed_contents=block.unparsed_contents, legacy_compiler_options=rule.legacy_compiler_options, diff --git a/scss/cssdefs.py b/scss/cssdefs.py index 34dfe91e..d94206b8 100644 --- a/scss/cssdefs.py +++ b/scss/cssdefs.py @@ -474,7 +474,6 @@ def unescape(string): _escape_chars_re = re.compile(r'([^-a-zA-Z0-9_])') _interpolate_re = re.compile(r'(#\{\s*)?(\$[-\w]+)(?(1)\s*\})') _spaces_re = re.compile(r'\s+') -_expand_rules_space_re = re.compile(r'\s*{') _collapse_properties_space_re = re.compile(r'([:#])\s*{') _variable_re = re.compile('^\\$[-a-zA-Z0-9_]+$') diff --git a/scss/errors.py b/scss/errors.py index b06793cd..6d3960e1 100644 --- a/scss/errors.py +++ b/scss/errors.py @@ -86,9 +86,12 @@ def format_sass_stack(self): last_file = self.rule_stack[0].source_file # TODO this could go away if rules knew their import chains... - # TODO mixins and the like here too? - # TODO the line number is wrong here, since this doesn't include the - # *block* being parsed! + # TODO this doesn't mention mixins or function calls. really need to + # track the call stack better. atm we skip other calls in the same + # file because most of them are just nesting, but they might not be! + # TODO the line number is wrong here for @imports, because we don't + # have access to the UnparsedBlock representing the import! + # TODO @content is completely broken; it's basically textual inclusion for rule in self.rule_stack[1:]: if rule.source_file is not last_file: ret.extend(( diff --git a/scss/rule.py b/scss/rule.py index e39e121e..712f4ba8 100644 --- a/scss/rule.py +++ b/scss/rule.py @@ -34,6 +34,7 @@ class SassRule(object): def __init__( self, source_file, import_key=None, unparsed_contents=None, + num_header_lines=0, options=None, legacy_compiler_options=None, properties=None, namespace=None, lineno=0, extends_selectors=frozenset(), @@ -48,6 +49,7 @@ def __init__( self.import_key = import_key self.lineno = lineno + self.num_header_lines = num_header_lines self.unparsed_contents = unparsed_contents self.legacy_compiler_options = legacy_compiler_options or {} self.options = options or {} @@ -211,6 +213,7 @@ class BlockHeader(object): @classmethod def parse(cls, prop, has_contents=False): + num_lines = prop.count('\n') prop = prop.strip() # Simple pre-processing @@ -247,25 +250,27 @@ def parse(cls, prop, has_contents=False): directive, argument = prop, None directive = directive.lower() - return BlockAtRuleHeader(directive, argument) + return BlockAtRuleHeader(directive, argument, num_lines) elif prop.split(None, 1)[0].endswith(':'): # Syntax is ": [prop]" -- if the optional prop exists, it # becomes the first rule with no suffix scope, unscoped_value = prop.split(':', 1) scope = scope.rstrip() unscoped_value = unscoped_value.lstrip() - return BlockScopeHeader(scope, unscoped_value) + return BlockScopeHeader(scope, unscoped_value, num_lines) else: - return BlockSelectorHeader(prop) + return BlockSelectorHeader(prop, num_lines) class BlockAtRuleHeader(BlockHeader): is_atrule = True - def __init__(self, directive, argument): + def __init__(self, directive, argument, num_lines=0): self.directive = directive self.argument = argument + self.num_lines = num_lines + def __repr__(self): return "<%s %r %r>" % (type(self).__name__, self.directive, self.argument) @@ -279,9 +284,11 @@ def render(self): class BlockSelectorHeader(BlockHeader): is_selector = True - def __init__(self, selectors): + def __init__(self, selectors, num_lines=0): self.selectors = tuple(selectors) + self.num_lines = num_lines + def __repr__(self): return "<%s %r>" % (type(self).__name__, self.selectors) @@ -295,7 +302,7 @@ def render(self, sep=', ', super_selector=''): class BlockScopeHeader(BlockHeader): is_scope = True - def __init__(self, scope, unscoped_value): + def __init__(self, scope, unscoped_value, num_lines=0): self.scope = scope if unscoped_value: @@ -303,6 +310,8 @@ def __init__(self, scope, unscoped_value): else: self.unscoped_value = None + self.num_lines = num_lines + class UnparsedBlock(object): """A Sass block whose contents have not yet been parsed. @@ -329,7 +338,8 @@ def __init__(self, parent_rule, lineno, prop, unparsed_contents): self.header = BlockHeader.parse(prop, has_contents=bool(unparsed_contents)) # Basic properties - self.lineno = lineno + self.lineno = ( + parent_rule.lineno - parent_rule.num_header_lines + lineno - 1) self.prop = prop self.unparsed_contents = unparsed_contents diff --git a/scss/source.py b/scss/source.py index 7f99c1f1..462278df 100644 --- a/scss/source.py +++ b/scss/source.py @@ -12,7 +12,7 @@ from scss.cssdefs import ( _ml_comment_re, _sl_comment_re, - _expand_rules_space_re, _collapse_properties_space_re, + _collapse_properties_space_re, _strings_re, ) from scss.cssdefs import determine_encoding @@ -261,7 +261,7 @@ def parse_scss_line(self, line, state): if line is None: line = '' - line = state['line_buffer'] + line.rstrip() # remove EOL character + line = state['line_buffer'] + line if line and line[-1] == '\\': state['line_buffer'] = line[:-1] @@ -274,10 +274,8 @@ def parse_scss_line(self, line, state): state['prev_line'] = line - if output: - output += '\n' - ret += output - + ret += output + ret += '\n' return ret def parse_sass_line(self, line, state): @@ -286,7 +284,7 @@ def parse_sass_line(self, line, state): if line is None: line = '' - line = state['line_buffer'] + line.rstrip() # remove EOL character + line = state['line_buffer'] + line if line and line[-1] == '\\': state['line_buffer'] = line[:-1] @@ -327,9 +325,8 @@ def parse_sass_line(self, line, state): state['prev_indent'] = indent state['prev_line'] = line - if output: - output += '\n' - ret += output + ret += output + ret += '\n' return ret def prepare_source(self, codestr, sass=False): @@ -351,6 +348,9 @@ def prepare_source(self, codestr, sass=False): # parse the last line stored in prev_line buffer codestr += parse_line(None, state) + # pop off the extra \n parse_line puts at the beginning + codestr = codestr[1:] + # protects codestr: "..." strings codestr = _strings_re.sub( lambda m: _reverse_safe_strings_re.sub( @@ -366,9 +366,6 @@ def prepare_source(self, codestr, sass=False): codestr = _safe_strings_re.sub( lambda m: _safe_strings[m.group(0)], codestr) - # expand the space in rules - codestr = _expand_rules_space_re.sub(' {', codestr) - # collapse the space in properties blocks codestr = _collapse_properties_space_re.sub(r'\1{', codestr)