<commit_before>from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

from collections import defaultdict
from enum import Enum
import glob
from itertools import product
import logging
import os.path
import re
import sys
import warnings

import six

import scss.config as config
from scss.cssdefs import _spaces_re
from scss.cssdefs import _escape_chars_re
from scss.cssdefs import _prop_split_re
from scss.errors import SassError
from scss.expression import Calculator
from scss.functions import ALL_BUILTINS_LIBRARY
from scss.functions.compass.sprites import sprite_map
from scss.rule import Namespace
from scss.rule import SassRule
from scss.rule import UnparsedBlock
from scss.selector import Selector
from scss.source import SourceFile
from scss.types import Number
from scss.types import Boolean
from scss.types import String
from scss.types import List
from scss.types import Null
from scss.types import Undefined
from scss.util import dequote
from scss.util import normalize_var  # TODO put in...  namespace maybe?

try:
    # Use C version if available
    from scss._speedups import locate_blocks
except ImportError:
    from scss._native import locate_blocks


# TODO should mention logging for the programmatic interface in the
# documentation
log = logging.getLogger(__name__)


# TODO produce this
# TODO is this a Library (can't define values), a Namespace (lot of
# semi-private functionality, no support for magic imports), or an Extension
# (doesn't exist yet)?
DEFAULT_NAMESPACE = Namespace(functions=ALL_BUILTINS_LIBRARY)


_default_rule_re = re.compile(r'(?i)\s+!default\Z')
_xcss_extends_re = re.compile(r'\s+extends\s+')


class OutputStyle(Enum):
    nested = ()
    compact = ()
    compressed = ()
    expanded = ()

    legacy = ()  # ???


class Compiler(object):
    """A Sass compiler.  Stores settings and knows how to fire off a
    compilation.  Main entry point into compiling Sass.
    """
    def __init__(
            self, root='', search_path=('',),
            namespace=DEFAULT_NAMESPACE, output_style='nested',
            generate_source_map=False,
            live_errors=False, warn_unused_imports=False,
            super_selector='',
        ):
        """Configure a compiler.

        :param root: Directory to treat as the "project root".  Search paths
            and some custom extensions (e.g. Compass) are relative to this
            directory.  Defaults to the current directory.
        :param search_path: List of paths to search for ``@import``s, relative
            to ``root``.  Absolute and parent paths are allowed here, but
            ``@import`` will refuse to load files that aren't in one of the
            directories here.  Defaults to only the root.
        :param namespace: Global namespace to inject into compiled Sass.  See
            the Namespace documentation for details.
        :type namespace: :class:`Namespace`
        """
        self.root = root
        self.search_path = search_path
        self.namespace = namespace
        self.output_style = output_style
        self.generate_source_map = generate_source_map
        self.live_errors = live_errors
        self.warn_unused_imports = warn_unused_imports
        self.super_selector = super_selector

        self.calculator = Calculator()

    def make_compilation(self):
        return Compilation(self)

    def compile(self):
        compilation = self.make_compilation()
        compilation.run()


class Compilation(object):
    """A single run of a compiler."""
    def __init__(self, compiler):
        self.compiler = compiler

        self.root_namespace = compiler.namespace

        self.sources = []
        self.source_index = {}
        self.rules = []

    def add_source(self, source):
        if source.filename in self.source_index:
            raise KeyError("Duplicate filename %r" % source.filename)
        self.sources.append(source)
        self.source_index[source.filename] = source

    def run(self):
        # this will compile and manage rule: child objects inside of a node
        self.parse_children()

        # this will manage @extends
        self.apply_extends()

        rules_by_file, css_files = self.parse_properties()

        all_rules = 0
        all_selectors = 0
        exceeded = ''
        final_cont = ''
        files = len(css_files)
        for source_file in css_files:
            rules = rules_by_file[source_file]
            fcont, total_rules, total_selectors = self.create_css(rules)
            all_rules += total_rules
            all_selectors += total_selectors
            # TODO i would love for the output of this function to be something
            # useful for producing stats, so this stuff can live on the Scss
            # class only
            if not exceeded and all_selectors > 4095:
                exceeded = " (IE exceeded!)"
                log.error("Maximum number of supported selectors in Internet Explorer (4095) exceeded!")
            if files > 1 and self.compiler.generate_source_map:
                if source_file.is_string:
                    final_cont += "/* %s %s generated add up to a total of %s %s accumulated%s */\n" % (
                        total_selectors,
                        'selector' if total_selectors == 1 else 'selectors',
                        all_selectors,
                        'selector' if all_selectors == 1 else 'selectors',
                        exceeded)
                else:
                    final_cont += "/* %s %s generated from '%s' add up to a total of %s %s accumulated%s */\n" % (
                        total_selectors,
                        'selector' if total_selectors == 1 else 'selectors',
                        source_file.filename,
                        all_selectors,
                        'selector' if all_selectors == 1 else 'selectors',
                        exceeded)
            final_cont += fcont

        return final_cont

    def compile(self, *args, **kwargs):
        try:
            return self.Compilation(*args, **kwargs)
        except SassError as e:
            if self.live_errors:
                # TODO should this setting also capture and display warnings?
                return e.to_css()
            else:
                raise

    def parse_selectors(self, raw_selectors):
        """
        Parses out the old xCSS "foo extends bar" syntax.

        Returns a 2-tuple: a set of selectors, and a set of extended selectors.
        """
        # Fix tabs and spaces in selectors
        raw_selectors = _spaces_re.sub(' ', raw_selectors)

        parts = _xcss_extends_re.split(raw_selectors, 1)  # handle old xCSS extends
        if len(parts) > 1:
            unparsed_selectors, unsplit_parents = parts
            # Multiple `extends` are delimited by `&`
            unparsed_parents = unsplit_parents.split('&')
        else:
            unparsed_selectors, = parts
            unparsed_parents = ()

        selectors = Selector.parse_many(unparsed_selectors)
        parents = [Selector.parse_one(parent) for parent in unparsed_parents]

        return selectors, parents

    # @print_timing(3)
    def parse_children(self, scope=None):
        children = []
        root_namespace = self.root_namespace
        for source_file in self.sources:
            rule = SassRule(
                source_file=source_file,

                unparsed_contents=source_file.contents,
                namespace=root_namespace,
            )
            self.rules.append(rule)
            children.append(rule)

        for rule in children:
            self.manage_children(rule, scope)

        self._warn_unused_imports(self.rules[0])

    def _warn_unused_imports(self, rule):
        if not rule.legacy_compiler_options.get(
                'warn_unused', self.compiler.warn_unused_imports):
            return

        for name, file_and_line in rule.namespace.unused_imports():
            log.warn("Unused @import: '%s' (%s)", name, file_and_line)

    # @print_timing(4)
    def manage_children(self, rule, scope):
        try:
            self._manage_children_impl(rule, scope)
        except SassReturn:
            raise
        except SassError as e:
            e.add_rule(rule)
            raise
        except Exception as e:
            raise SassError(e, rule=rule)

    def _manage_children_impl(self, rule, scope):
        calculator = Calculator(rule.namespace)

        for c_lineno, c_property, c_codestr in locate_blocks(rule.unparsed_contents):
            block = UnparsedBlock(rule, c_lineno, c_property, c_codestr)

            ####################################################################
            # At (@) blocks
            if block.is_atrule:
                code = block.directive
                code = '_at_' + code.lower().replace(' ', '_')[1:]
                try:
                    getattr(self, code)(calculator, rule, scope, block)
                except AttributeError:
                    if block.unparsed_contents is None:
                        rule.properties.append((block.prop, None))
                    elif scope is None:  # needs to have no scope to crawl down the nested rules
                        self._nest_at_rules(rule, scope, block)

            ####################################################################
            # Properties
            elif block.unparsed_contents is None:
                self._get_properties(rule, scope, block)

            # Nested properties
            elif block.is_scope:
                if block.header.unscoped_value:
                    # Possibly deal with default unscoped value
                    self._get_properties(rule, scope, block)

                rule.unparsed_contents = block.unparsed_contents
                subscope = (scope or '') + block.header.scope + '-'
                self.manage_children(rule, subscope)

            ####################################################################
            # Nested rules
            elif scope is None:  # needs to have no scope to crawl down the nested rules
                self._nest_rules(rule, scope, block)

    def _at_warn(self, calculator, rule, scope, block):
        """
        Implements @warn
        """
        value = calculator.calculate(block.argument)
        log.warn(repr(value))

    def _at_print(self, calculator, rule, scope, block):
        """
        Implements @print
        """
        value = calculator.calculate(block.argument)
        sys.stderr.write("%s\n" % value)

    def _at_raw(self, calculator, rule, scope, block):
        """
        Implements @raw
        """
        value = calculator.calculate(block.argument)
        sys.stderr.write("%s\n" % repr(value))

    def _at_dump_context(self, calculator, rule, scope, block):
        """
        Implements @dump_context
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._variables))

    def _at_dump_functions(self, calculator, rule, scope, block):
        """
        Implements @dump_functions
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._functions))

    def _at_dump_mixins(self, calculator, rule, scope, block):
        """
        Implements @dump_mixins
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._mixins))

    def _at_dump_imports(self, calculator, rule, scope, block):
        """
        Implements @dump_imports
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._imports))

    def _at_dump_options(self, calculator, rule, scope, block):
        """
        Implements @dump_options
        """
        sys.stderr.write("%s\n" % repr(rule.options))

    def _at_debug(self, calculator, rule, scope, block):
        """
        Implements @debug
        """
        setting = block.argument.strip()
        if setting.lower() in ('1', 'true', 't', 'yes', 'y', 'on'):
            setting = True
        elif setting.lower() in ('0', 'false', 'f', 'no', 'n', 'off', 'undefined'):
            setting = False
        config.DEBUG = setting
        log.info("Debug mode is %s", 'On' if config.DEBUG else 'Off')

    def _at_pdb(self, calculator, rule, scope, block):
        """
        Implements @pdb
        """
        try:
            import ipdb as pdb
        except ImportError:
            import pdb
        pdb.set_trace()

    def _at_extend(self, calculator, rule, scope, block):
        """
        Implements @extend
        """
        from scss.selector import Selector
        selectors = calculator.apply_vars(block.argument)
        # XXX this no longer handles `&`, which is from xcss
        rule.extends_selectors.extend(Selector.parse_many(selectors))
        #rule.extends_selectors.update(p.strip() for p in selectors.replace(',', '&').split('&'))
        #rule.extends_selectors.discard('')

    def _at_return(self, calculator, rule, scope, block):
        """
        Implements @return
        """
        # TODO should assert this only happens within a @function
        ret = calculator.calculate(block.argument)
        raise SassReturn(ret)

    # @print_timing(10)
    def _at_option(self, calculator, rule, scope, block):
        """
        Implements @option
        """
        # TODO This only actually supports "style" (which only really makes
        # sense as the first thing in a single input file) or "warn_unused"
        # (which only makes sense at file level /at best/).  Explore either
        # replacing this with a better mechanism or dropping it entirely.
        # Note also that all rules share the same underlying legacy option
        # dict, so the rules aren't even lexically scoped like you might think,
        # and @importing a file can change the compiler!  That seems totally
        # wrong.
        for option in block.argument.split(','):
            key, colon, value = option.partition(':')
            key = key.strip().lower().replace('-', '_')
            value = value.strip().lower()

            if value in ('1', 'true', 't', 'yes', 'y', 'on'):
                value = True
            elif value in ('0', 'false', 'f', 'no', 'n', 'off', 'undefined'):
                value = False
            elif not colon:
                value = True

            if key == 'compress':
                log.warn("The 'compress' @option is deprecated.  Please use 'style' instead.")
                key = 'style'
                value = 'compressed' if value else 'legacy'

            if key in ('short_colors', 'reverse_colors'):
                log.warn("The '{0}' @option no longer has any effect.".format(key))
                return
            elif key == 'style':
                try:
                    OutputStyle[value]
                except KeyError:
                    raise SassError("No such output style: {0}".format(value))
            elif key in ('warn_unused', 'control_scoping'):
                # TODO deprecate control_scoping?  or add it to compiler?
                if not isinstance(value, bool):
                    raise SassError("The '{0}' @option requires a bool, not {1!r}".format(key, value))
            else:
                raise SassError("Unknown @option: {0}".format(key))

            rule.legacy_compiler_options[key] = value

    def _get_funct_def(self, rule, calculator, argument):
        funct, lpar, argstr = argument.partition('(')
        funct = calculator.do_glob_math(funct)
        funct = normalize_var(funct.strip())
        argstr = argstr.strip()

        # Parse arguments with the argspec rule
        if lpar:
            if not argstr.endswith(')'):
                raise SyntaxError("Expected ')', found end of line for %s (%s)" % (funct, rule.file_and_line))
            argstr = argstr[:-1].strip()
        else:
            # Whoops, no parens at all.  That's like calling with no arguments.
            argstr = ''

        argstr = calculator.do_glob_math(argstr)
        argspec_node = calculator.parse_expression(argstr, target='goal_argspec')
        return funct, argspec_node

    def _populate_namespace_from_call(self, name, callee_namespace, mixin, args, kwargs):
        # Mutation protection
        args = list(args)
        kwargs = dict(kwargs)

        #m_params = mixin[0]
        #m_defaults = mixin[1]
        #m_codestr = mixin[2]
        pristine_callee_namespace = mixin[3]
        callee_argspec = mixin[4]
        import_key = mixin[5]

        callee_calculator = Calculator(callee_namespace)

        # Populate the mixin/function's namespace with its arguments
        for var_name, node in callee_argspec.iter_def_argspec():
            if args:
                # If there are positional arguments left, use the first
                value = args.pop(0)
            elif var_name in kwargs:
                # Try keyword arguments
                value = kwargs.pop(var_name)
            elif node is not None:
                # OK, there's a default argument; try that
                # DEVIATION: this allows argument defaults to refer to earlier
                # argument values
                value = node.evaluate(callee_calculator, divide=True)
            else:
                # TODO this should raise
                value = Undefined()

            callee_namespace.set_variable(var_name, value, local_only=True)

        if callee_argspec.slurp:
            # Slurpy var gets whatever is left
            callee_namespace.set_variable(
                callee_argspec.slurp.name,
                List(args, use_comma=True))
            args = []
        elif callee_argspec.inject:
            # Callee namespace gets all the extra kwargs whether declared or
            # not
            for var_name, value in kwargs.items():
                callee_namespace.set_variable(var_name, value, local_only=True)
            kwargs = {}

        # TODO would be nice to say where the mixin/function came from
        if kwargs:
            raise NameError("%s has no such argument %s" % (name, kwargs.keys()[0]))

        if args:
            raise NameError("%s received extra arguments: %r" % (name, args))

        pristine_callee_namespace.use_import(import_key)
        return callee_namespace

    # @print_timing(10)
    def _at_function(self, calculator, rule, scope, block):
        """
        Implements @mixin and @function
        """
        if not block.argument:
            raise SyntaxError("%s requires a function name (%s)" % (block.directive, rule.file_and_line))

        funct, argspec_node = self._get_funct_def(rule, calculator, block.argument)

        defaults = {}
        new_params = []

        for var_name, default in argspec_node.iter_def_argspec():
            new_params.append(var_name)
            if default is not None:
                defaults[var_name] = default

        # TODO a function or mixin is re-parsed every time it's called; there's
        # no AST for anything but expressions  :(
        mixin = [rule.source_file, block.lineno, block.unparsed_contents, rule.namespace, argspec_node, rule.import_key]
        if block.directive == '@function':
            def _call(mixin):
                def __call(namespace, *args, **kwargs):
                    source_file = mixin[0]
                    lineno = mixin[1]
                    m_codestr = mixin[2]
                    pristine_callee_namespace = mixin[3]
                    callee_namespace = pristine_callee_namespace.derive()

                    # TODO CallOp converts Sass names to Python names, so we
                    # have to convert them back to Sass names.  would be nice
                    # to avoid this back-and-forth somehow
                    kwargs = dict(
                        (normalize_var('$' + key), value)
                        for (key, value) in kwargs.items())

                    self._populate_namespace_from_call(
                        "Function {0}".format(funct),
                        callee_namespace, mixin, args, kwargs)

                    _rule = SassRule(
                        source_file=source_file,
                        lineno=lineno,
                        unparsed_contents=m_codestr,
                        namespace=callee_namespace,

                        # rule
                        import_key=rule.import_key,
                        legacy_compiler_options=rule.legacy_compiler_options,
                        options=rule.options,
                        properties=rule.properties,
                        extends_selectors=rule.extends_selectors,
                        ancestry=rule.ancestry,
                        nested=rule.nested,
                    )
                    try:
                        self.manage_children(_rule, scope)
                    except SassReturn as e:
                        return e.retval
                    else:
                        return Null()
                return __call
            _mixin = _call(mixin)
            _mixin.mixin = mixin
            mixin = _mixin

        if block.directive == '@mixin':
            add = rule.namespace.set_mixin
        elif block.directive == '@function':
            add = rule.namespace.set_function

        # Register the mixin for every possible arity it takes
        if argspec_node.slurp or argspec_node.inject:
            add(funct, None, mixin)
        else:
            while len(new_params):
                add(funct, len(new_params), mixin)
                param = new_params.pop()
                if param not in defaults:
                    break
            if not new_params:
                add(funct, 0, mixin)
    _at_mixin = _at_function

    # @print_timing(10)
    def _at_include(self, calculator, rule, scope, block):
        """
        Implements @include, for @mixins
        """
        caller_namespace = rule.namespace
        caller_calculator = Calculator(caller_namespace)
        funct, caller_argspec = self._get_funct_def(rule, caller_calculator, block.argument)

        # Render the passed arguments, using the caller's namespace
        args, kwargs = caller_argspec.evaluate_call_args(caller_calculator)

        argc = len(args) + len(kwargs)
        try:
            mixin = caller_namespace.mixin(funct, argc)
        except KeyError:
            try:
                # TODO maybe? don't do this, once '...' works
                # Fallback to single parameter:
                mixin = caller_namespace.mixin(funct, 1)
            except KeyError:
                log.error("Mixin not found: %s:%d (%s)", funct, argc, rule.file_and_line, extra={'stack': True})
                return
            else:
                args = [List(args, use_comma=True)]
                # TODO what happens to kwargs?

        source_file = mixin[0]
        lineno = mixin[1]
        m_codestr = mixin[2]
        pristine_callee_namespace = mixin[3]
        callee_argspec = mixin[4]
        if caller_argspec.inject and callee_argspec.inject:
            # DEVIATION: Pass the ENTIRE local namespace to the mixin (yikes)
            callee_namespace = Namespace.derive_from(
                caller_namespace,
                pristine_callee_namespace)
        else:
            callee_namespace = pristine_callee_namespace.derive()

        self._populate_namespace_from_call(
            "Mixin {0}".format(funct),
            callee_namespace, mixin, args, kwargs)

        _rule = SassRule(
            # These must be file and line in which the @include occurs
            source_file=rule.source_file,
            lineno=rule.lineno,

            # These must be file and line in which the @mixin was defined
            from_source_file=source_file,
            from_lineno=lineno,

            unparsed_contents=m_codestr,
            namespace=callee_namespace,

            # rule
            import_key=rule.import_key,
            legacy_compiler_options=rule.legacy_compiler_options,
            options=rule.options,
            properties=rule.properties,
            extends_selectors=rule.extends_selectors,
            ancestry=rule.ancestry,
            nested=rule.nested,
        )

        _rule.options['@content'] = block.unparsed_contents
        self.manage_children(_rule, scope)

    # @print_timing(10)
    def _at_content(self, calculator, rule, scope, block):
        """
        Implements @content
        """
        if '@content' not in rule.options:
            log.error("Content string not found for @content (%s)", rule.file_and_line)
        rule.unparsed_contents = rule.options.pop('@content', '')
        self.manage_children(rule, scope)

    # @print_timing(10)
    def _at_import(self, calculator, rule, scope, block):
        """
        Implements @import
        Load and import mixins and functions and rules
        """
        full_filename = None
        names = block.argument.split(',')
        for name in names:
            name = dequote(name.strip())

            # Protect against going to prohibited places...
            if any(scary_token in name for scary_token in ('..', '://', 'url(')):
                rule.properties.append((block.prop, None))
                warnings.warn("Ignored import: %s" % name, RuntimeWarning)
                continue

            source = None
            full_filename, seen_paths = self._find_import(rule, name, skip=rule.source_file.full_filename)

            if full_filename is None:
                i_codestr = self._at_magic_import(calculator, rule, scope, block)

                if i_codestr is not None:
                    source = SourceFile.from_string(i_codestr)

            elif full_filename in self.source_file_index:
                source = self.source_file_index[full_filename]
            else:
                source = SourceFile.from_filename(full_filename)
                self.add_source(source)

            if source is None:
                load_paths_msg = "\nLoad paths:\n\t%s" % "\n\t".join(seen_paths)
                raise IOError("File to import not found or unreadable: '%s' (%s)%s" % (name, rule.file_and_line, load_paths_msg))

            import_key = (name, source.parent_dir)
            if rule.namespace.has_import(import_key):
                # If already imported in this scope, skip
                continue

            _rule = SassRule(
                source_file=source,
                import_key=import_key,
                lineno=block.lineno,
                unparsed_contents=source.contents,<commit_msg>Fix an AttributeError in _at_import, and the swallowing thereof.<commit_after>from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

from collections import defaultdict
from enum import Enum
import glob
from itertools import product
import logging
import os.path
import re
import sys
import warnings

import six

import scss.config as config
from scss.cssdefs import _spaces_re
from scss.cssdefs import _escape_chars_re
from scss.cssdefs import _prop_split_re
from scss.errors import SassError
from scss.expression import Calculator
from scss.functions import ALL_BUILTINS_LIBRARY
from scss.functions.compass.sprites import sprite_map
from scss.rule import Namespace
from scss.rule import SassRule
from scss.rule import UnparsedBlock
from scss.selector import Selector
from scss.source import SourceFile
from scss.types import Number
from scss.types import Boolean
from scss.types import String
from scss.types import List
from scss.types import Null
from scss.types import Undefined
from scss.util import dequote
from scss.util import normalize_var  # TODO put in...  namespace maybe?

try:
    # Use C version if available
    from scss._speedups import locate_blocks
except ImportError:
    from scss._native import locate_blocks


# TODO should mention logging for the programmatic interface in the
# documentation
log = logging.getLogger(__name__)


# TODO produce this
# TODO is this a Library (can't define values), a Namespace (lot of
# semi-private functionality, no support for magic imports), or an Extension
# (doesn't exist yet)?
DEFAULT_NAMESPACE = Namespace(functions=ALL_BUILTINS_LIBRARY)


_default_rule_re = re.compile(r'(?i)\s+!default\Z')
_xcss_extends_re = re.compile(r'\s+extends\s+')


class OutputStyle(Enum):
    nested = ()
    compact = ()
    compressed = ()
    expanded = ()

    legacy = ()  # ???


class Compiler(object):
    """A Sass compiler.  Stores settings and knows how to fire off a
    compilation.  Main entry point into compiling Sass.
    """
    def __init__(
            self, root='', search_path=('',),
            namespace=DEFAULT_NAMESPACE, output_style='nested',
            generate_source_map=False,
            live_errors=False, warn_unused_imports=False,
            super_selector='',
        ):
        """Configure a compiler.

        :param root: Directory to treat as the "project root".  Search paths
            and some custom extensions (e.g. Compass) are relative to this
            directory.  Defaults to the current directory.
        :param search_path: List of paths to search for ``@import``s, relative
            to ``root``.  Absolute and parent paths are allowed here, but
            ``@import`` will refuse to load files that aren't in one of the
            directories here.  Defaults to only the root.
        :param namespace: Global namespace to inject into compiled Sass.  See
            the Namespace documentation for details.
        :type namespace: :class:`Namespace`
        """
        self.root = root
        self.search_path = search_path
        self.namespace = namespace
        self.output_style = output_style
        self.generate_source_map = generate_source_map
        self.live_errors = live_errors
        self.warn_unused_imports = warn_unused_imports
        self.super_selector = super_selector

        self.calculator = Calculator()

    def make_compilation(self):
        return Compilation(self)

    def compile(self):
        compilation = self.make_compilation()
        compilation.run()


class Compilation(object):
    """A single run of a compiler."""
    def __init__(self, compiler):
        self.compiler = compiler

        # TODO this needs a write barrier, so assignment can't overwrite what's
        # in the original namespace
        self.root_namespace = compiler.namespace

        self.sources = []
        self.source_index = {}
        self.rules = []

    def add_source(self, source):
        if source.filename in self.source_index:
            raise KeyError("Duplicate filename %r" % source.filename)
        self.sources.append(source)
        self.source_index[source.filename] = source

    def run(self):
        # this will compile and manage rule: child objects inside of a node
        self.parse_children()

        # this will manage @extends
        self.apply_extends()

        rules_by_file, css_files = self.parse_properties()

        all_rules = 0
        all_selectors = 0
        exceeded = ''
        final_cont = ''
        files = len(css_files)
        for source_file in css_files:
            rules = rules_by_file[source_file]
            fcont, total_rules, total_selectors = self.create_css(rules)
            all_rules += total_rules
            all_selectors += total_selectors
            # TODO i would love for the output of this function to be something
            # useful for producing stats, so this stuff can live on the Scss
            # class only
            if not exceeded and all_selectors > 4095:
                exceeded = " (IE exceeded!)"
                log.error("Maximum number of supported selectors in Internet Explorer (4095) exceeded!")
            if files > 1 and self.compiler.generate_source_map:
                if source_file.is_string:
                    final_cont += "/* %s %s generated add up to a total of %s %s accumulated%s */\n" % (
                        total_selectors,
                        'selector' if total_selectors == 1 else 'selectors',
                        all_selectors,
                        'selector' if all_selectors == 1 else 'selectors',
                        exceeded)
                else:
                    final_cont += "/* %s %s generated from '%s' add up to a total of %s %s accumulated%s */\n" % (
                        total_selectors,
                        'selector' if total_selectors == 1 else 'selectors',
                        source_file.filename,
                        all_selectors,
                        'selector' if all_selectors == 1 else 'selectors',
                        exceeded)
            final_cont += fcont

        return final_cont

    def compile(self, *args, **kwargs):
        try:
            return self.Compilation(*args, **kwargs)
        except SassError as e:
            if self.live_errors:
                # TODO should this setting also capture and display warnings?
                return e.to_css()
            else:
                raise

    def parse_selectors(self, raw_selectors):
        """
        Parses out the old xCSS "foo extends bar" syntax.

        Returns a 2-tuple: a set of selectors, and a set of extended selectors.
        """
        # Fix tabs and spaces in selectors
        raw_selectors = _spaces_re.sub(' ', raw_selectors)

        parts = _xcss_extends_re.split(raw_selectors, 1)  # handle old xCSS extends
        if len(parts) > 1:
            unparsed_selectors, unsplit_parents = parts
            # Multiple `extends` are delimited by `&`
            unparsed_parents = unsplit_parents.split('&')
        else:
            unparsed_selectors, = parts
            unparsed_parents = ()

        selectors = Selector.parse_many(unparsed_selectors)
        parents = [Selector.parse_one(parent) for parent in unparsed_parents]

        return selectors, parents

    # @print_timing(3)
    def parse_children(self, scope=None):
        children = []
        root_namespace = self.root_namespace
        for source_file in self.sources:
            rule = SassRule(
                source_file=source_file,

                unparsed_contents=source_file.contents,
                namespace=root_namespace,
            )
            self.rules.append(rule)
            children.append(rule)

        for rule in children:
            self.manage_children(rule, scope)

        self._warn_unused_imports(self.rules[0])

    def _warn_unused_imports(self, rule):
        if not rule.legacy_compiler_options.get(
                'warn_unused', self.compiler.warn_unused_imports):
            return

        for name, file_and_line in rule.namespace.unused_imports():
            log.warn("Unused @import: '%s' (%s)", name, file_and_line)

    # @print_timing(4)
    def manage_children(self, rule, scope):
        try:
            self._manage_children_impl(rule, scope)
        except SassReturn:
            raise
        except SassError as e:
            e.add_rule(rule)
            raise
        except Exception as e:
            raise SassError(e, rule=rule)

    def _manage_children_impl(self, rule, scope):
        calculator = Calculator(rule.namespace)

        for c_lineno, c_property, c_codestr in locate_blocks(rule.unparsed_contents):
            block = UnparsedBlock(rule, c_lineno, c_property, c_codestr)

            ####################################################################
            # At (@) blocks
            if block.is_atrule:
                code = block.directive
                code = '_at_' + code.lower().replace(' ', '_')[1:]
                try:
                    method = getattr(self, code)
                except AttributeError:
                    if block.unparsed_contents is None:
                        rule.properties.append((block.prop, None))
                    elif scope is None:  # needs to have no scope to crawl down the nested rules
                        self._nest_at_rules(rule, scope, block)
                else:
                    method(calculator, rule, scope, block)

            ####################################################################
            # Properties
            elif block.unparsed_contents is None:
                self._get_properties(rule, scope, block)

            # Nested properties
            elif block.is_scope:
                if block.header.unscoped_value:
                    # Possibly deal with default unscoped value
                    self._get_properties(rule, scope, block)

                rule.unparsed_contents = block.unparsed_contents
                subscope = (scope or '') + block.header.scope + '-'
                self.manage_children(rule, subscope)

            ####################################################################
            # Nested rules
            elif scope is None:  # needs to have no scope to crawl down the nested rules
                self._nest_rules(rule, scope, block)

    def _at_warn(self, calculator, rule, scope, block):
        """
        Implements @warn
        """
        value = calculator.calculate(block.argument)
        log.warn(repr(value))

    def _at_print(self, calculator, rule, scope, block):
        """
        Implements @print
        """
        value = calculator.calculate(block.argument)
        sys.stderr.write("%s\n" % value)

    def _at_raw(self, calculator, rule, scope, block):
        """
        Implements @raw
        """
        value = calculator.calculate(block.argument)
        sys.stderr.write("%s\n" % repr(value))

    def _at_dump_context(self, calculator, rule, scope, block):
        """
        Implements @dump_context
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._variables))

    def _at_dump_functions(self, calculator, rule, scope, block):
        """
        Implements @dump_functions
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._functions))

    def _at_dump_mixins(self, calculator, rule, scope, block):
        """
        Implements @dump_mixins
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._mixins))

    def _at_dump_imports(self, calculator, rule, scope, block):
        """
        Implements @dump_imports
        """
        sys.stderr.write("%s\n" % repr(rule.namespace._imports))

    def _at_dump_options(self, calculator, rule, scope, block):
        """
        Implements @dump_options
        """
        sys.stderr.write("%s\n" % repr(rule.options))

    def _at_debug(self, calculator, rule, scope, block):
        """
        Implements @debug
        """
        setting = block.argument.strip()
        if setting.lower() in ('1', 'true', 't', 'yes', 'y', 'on'):
            setting = True
        elif setting.lower() in ('0', 'false', 'f', 'no', 'n', 'off', 'undefined'):
            setting = False
        config.DEBUG = setting
        log.info("Debug mode is %s", 'On' if config.DEBUG else 'Off')

    def _at_pdb(self, calculator, rule, scope, block):
        """
        Implements @pdb
        """
        try:
            import ipdb as pdb
        except ImportError:
            import pdb
        pdb.set_trace()

    def _at_extend(self, calculator, rule, scope, block):
        """
        Implements @extend
        """
        from scss.selector import Selector
        selectors = calculator.apply_vars(block.argument)
        # XXX this no longer handles `&`, which is from xcss
        rule.extends_selectors.extend(Selector.parse_many(selectors))
        #rule.extends_selectors.update(p.strip() for p in selectors.replace(',', '&').split('&'))
        #rule.extends_selectors.discard('')

    def _at_return(self, calculator, rule, scope, block):
        """
        Implements @return
        """
        # TODO should assert this only happens within a @function
        ret = calculator.calculate(block.argument)
        raise SassReturn(ret)

    # @print_timing(10)
    def _at_option(self, calculator, rule, scope, block):
        """
        Implements @option
        """
        # TODO This only actually supports "style" (which only really makes
        # sense as the first thing in a single input file) or "warn_unused"
        # (which only makes sense at file level /at best/).  Explore either
        # replacing this with a better mechanism or dropping it entirely.
        # Note also that all rules share the same underlying legacy option
        # dict, so the rules aren't even lexically scoped like you might think,
        # and @importing a file can change the compiler!  That seems totally
        # wrong.
        for option in block.argument.split(','):
            key, colon, value = option.partition(':')
            key = key.strip().lower().replace('-', '_')
            value = value.strip().lower()

            if value in ('1', 'true', 't', 'yes', 'y', 'on'):
                value = True
            elif value in ('0', 'false', 'f', 'no', 'n', 'off', 'undefined'):
                value = False
            elif not colon:
                value = True

            if key == 'compress':
                log.warn("The 'compress' @option is deprecated.  Please use 'style' instead.")
                key = 'style'
                value = 'compressed' if value else 'legacy'

            if key in ('short_colors', 'reverse_colors'):
                log.warn("The '{0}' @option no longer has any effect.".format(key))
                return
            elif key == 'style':
                try:
                    OutputStyle[value]
                except KeyError:
                    raise SassError("No such output style: {0}".format(value))
            elif key in ('warn_unused', 'control_scoping'):
                # TODO deprecate control_scoping?  or add it to compiler?
                if not isinstance(value, bool):
                    raise SassError("The '{0}' @option requires a bool, not {1!r}".format(key, value))
            else:
                raise SassError("Unknown @option: {0}".format(key))

            rule.legacy_compiler_options[key] = value

    def _get_funct_def(self, rule, calculator, argument):
        funct, lpar, argstr = argument.partition('(')
        funct = calculator.do_glob_math(funct)
        funct = normalize_var(funct.strip())
        argstr = argstr.strip()

        # Parse arguments with the argspec rule
        if lpar:
            if not argstr.endswith(')'):
                raise SyntaxError("Expected ')', found end of line for %s (%s)" % (funct, rule.file_and_line))
            argstr = argstr[:-1].strip()
        else:
            # Whoops, no parens at all.  That's like calling with no arguments.
            argstr = ''

        argstr = calculator.do_glob_math(argstr)
        argspec_node = calculator.parse_expression(argstr, target='goal_argspec')
        return funct, argspec_node

    def _populate_namespace_from_call(self, name, callee_namespace, mixin, args, kwargs):
        # Mutation protection
        args = list(args)
        kwargs = dict(kwargs)

        #m_params = mixin[0]
        #m_defaults = mixin[1]
        #m_codestr = mixin[2]
        pristine_callee_namespace = mixin[3]
        callee_argspec = mixin[4]
        import_key = mixin[5]

        callee_calculator = Calculator(callee_namespace)

        # Populate the mixin/function's namespace with its arguments
        for var_name, node in callee_argspec.iter_def_argspec():
            if args:
                # If there are positional arguments left, use the first
                value = args.pop(0)
            elif var_name in kwargs:
                # Try keyword arguments
                value = kwargs.pop(var_name)
            elif node is not None:
                # OK, there's a default argument; try that
                # DEVIATION: this allows argument defaults to refer to earlier
                # argument values
                value = node.evaluate(callee_calculator, divide=True)
            else:
                # TODO this should raise
                value = Undefined()

            callee_namespace.set_variable(var_name, value, local_only=True)

        if callee_argspec.slurp:
            # Slurpy var gets whatever is left
            callee_namespace.set_variable(
                callee_argspec.slurp.name,
                List(args, use_comma=True))
            args = []
        elif callee_argspec.inject:
            # Callee namespace gets all the extra kwargs whether declared or
            # not
            for var_name, value in kwargs.items():
                callee_namespace.set_variable(var_name, value, local_only=True)
            kwargs = {}

        # TODO would be nice to say where the mixin/function came from
        if kwargs:
            raise NameError("%s has no such argument %s" % (name, kwargs.keys()[0]))

        if args:
            raise NameError("%s received extra arguments: %r" % (name, args))

        pristine_callee_namespace.use_import(import_key)
        return callee_namespace

    # @print_timing(10)
    def _at_function(self, calculator, rule, scope, block):
        """
        Implements @mixin and @function
        """
        if not block.argument:
            raise SyntaxError("%s requires a function name (%s)" % (block.directive, rule.file_and_line))

        funct, argspec_node = self._get_funct_def(rule, calculator, block.argument)

        defaults = {}
        new_params = []

        for var_name, default in argspec_node.iter_def_argspec():
            new_params.append(var_name)
            if default is not None:
                defaults[var_name] = default

        # TODO a function or mixin is re-parsed every time it's called; there's
        # no AST for anything but expressions  :(
        mixin = [rule.source_file, block.lineno, block.unparsed_contents, rule.namespace, argspec_node, rule.import_key]
        if block.directive == '@function':
            def _call(mixin):
                def __call(namespace, *args, **kwargs):
                    source_file = mixin[0]
                    lineno = mixin[1]
                    m_codestr = mixin[2]
                    pristine_callee_namespace = mixin[3]
                    callee_namespace = pristine_callee_namespace.derive()

                    # TODO CallOp converts Sass names to Python names, so we
                    # have to convert them back to Sass names.  would be nice
                    # to avoid this back-and-forth somehow
                    kwargs = dict(
                        (normalize_var('$' + key), value)
                        for (key, value) in kwargs.items())

                    self._populate_namespace_from_call(
                        "Function {0}".format(funct),
                        callee_namespace, mixin, args, kwargs)

                    _rule = SassRule(
                        source_file=source_file,
                        lineno=lineno,
                        unparsed_contents=m_codestr,
                        namespace=callee_namespace,

                        # rule
                        import_key=rule.import_key,
                        legacy_compiler_options=rule.legacy_compiler_options,
                        options=rule.options,
                        properties=rule.properties,
                        extends_selectors=rule.extends_selectors,
                        ancestry=rule.ancestry,
                        nested=rule.nested,
                    )
                    try:
                        self.manage_children(_rule, scope)
                    except SassReturn as e:
                        return e.retval
                    else:
                        return Null()
                return __call
            _mixin = _call(mixin)
            _mixin.mixin = mixin
            mixin = _mixin

        if block.directive == '@mixin':
            add = rule.namespace.set_mixin
        elif block.directive == '@function':
            add = rule.namespace.set_function

        # Register the mixin for every possible arity it takes
        if argspec_node.slurp or argspec_node.inject:
            add(funct, None, mixin)
        else:
            while len(new_params):
                add(funct, len(new_params), mixin)
                param = new_params.pop()
                if param not in defaults:
                    break
            if not new_params:
                add(funct, 0, mixin)
    _at_mixin = _at_function

    # @print_timing(10)
    def _at_include(self, calculator, rule, scope, block):
        """
        Implements @include, for @mixins
        """
        caller_namespace = rule.namespace
        caller_calculator = Calculator(caller_namespace)
        funct, caller_argspec = self._get_funct_def(rule, caller_calculator, block.argument)

        # Render the passed arguments, using the caller's namespace
        args, kwargs = caller_argspec.evaluate_call_args(caller_calculator)

        argc = len(args) + len(kwargs)
        try:
            mixin = caller_namespace.mixin(funct, argc)
        except KeyError:
            try:
                # TODO maybe? don't do this, once '...' works
                # Fallback to single parameter:
                mixin = caller_namespace.mixin(funct, 1)
            except KeyError:
                log.error("Mixin not found: %s:%d (%s)", funct, argc, rule.file_and_line, extra={'stack': True})
                return
            else:
                args = [List(args, use_comma=True)]
                # TODO what happens to kwargs?

        source_file = mixin[0]
        lineno = mixin[1]
        m_codestr = mixin[2]
        pristine_callee_namespace = mixin[3]
        callee_argspec = mixin[4]
        if caller_argspec.inject and callee_argspec.inject:
            # DEVIATION: Pass the ENTIRE local namespace to the mixin (yikes)
            callee_namespace = Namespace.derive_from(
                caller_namespace,
                pristine_callee_namespace)
        else:
            callee_namespace = pristine_callee_namespace.derive()

        self._populate_namespace_from_call(
            "Mixin {0}".format(funct),
            callee_namespace, mixin, args, kwargs)

        _rule = SassRule(
            # These must be file and line in which the @include occurs
            source_file=rule.source_file,
            lineno=rule.lineno,

            # These must be file and line in which the @mixin was defined
            from_source_file=source_file,
            from_lineno=lineno,

            unparsed_contents=m_codestr,
            namespace=callee_namespace,

            # rule
            import_key=rule.import_key,
            legacy_compiler_options=rule.legacy_compiler_options,
            options=rule.options,
            properties=rule.properties,
            extends_selectors=rule.extends_selectors,
            ancestry=rule.ancestry,
            nested=rule.nested,
        )

        _rule.options['@content'] = block.unparsed_contents
        self.manage_children(_rule, scope)

    # @print_timing(10)
    def _at_content(self, calculator, rule, scope, block):
        """
        Implements @content
        """
        if '@content' not in rule.options:
            log.error("Content string not found for @content (%s)", rule.file_and_line)
        rule.unparsed_contents = rule.options.pop('@content', '')
        self.manage_children(rule, scope)

    # @print_timing(10)
    def _at_import(self, calculator, rule, scope, block):
        """
        Implements @import
        Load and import mixins and functions and rules
        """
        full_filename = None
        names = block.argument.split(',')
        for name in names:
            name = dequote(name.strip())

            # Protect against going to prohibited places...
            if any(scary_token in name for scary_token in ('..', '://', 'url(')):
                rule.properties.append((block.prop, None))
                warnings.warn("Ignored import: %s" % name, RuntimeWarning)
                continue

            source = None
            full_filename, seen_paths = self._find_import(rule, name, skip=rule.source_file.full_filename)

            if full_filename is None:
                i_codestr = self._at_magic_import(calculator, rule, scope, block)

                if i_codestr is not None:
                    source = SourceFile.from_string(i_codestr)

            elif full_filename in self.source_index:
                source = self.source_index[full_filename]
            else:
                source = SourceFile.from_filename(full_filename)
                self.add_source(source)

            if source is None:
                load_paths_msg = "\nLoad paths:\n\t%s" % "\n\t".join(seen_paths)
                raise IOError("File to import not found or unreadable: '%s' (%s)%s" % (name, rule.file_and_line, load_paths_msg))

            import_key = (name, source.parent_dir)
            if rule.namespace.has_import(import_key):
                # If already imported in this scope, skip
                continue

            _rule = SassRule(
                source_file=source,
                import_key=import_key,
                lineno=block.lineno,
                unparsed_contents=source.contents,
{'fraction_non_alphanumeric': 0.09478859759415993, 'fraction_numerical': 0.002364554206466868, 'mean_word_length': 1.933230582925084, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2592239', 'n_tokens_mistral': 14791, 'n_tokens_neox': 14151, 'n_words': 3967}