diff --git a/.flake8 b/.flake8 index 673525b..2484977 100644 --- a/.flake8 +++ b/.flake8 @@ -10,3 +10,4 @@ ignore = # This catches line breaks after "and" / "or" as a means of breaking up # long if statements, which PEP 8 explicitly encourages. W504 +exclude=aip_site/vendor diff --git a/MANIFEST.in b/MANIFEST.in index 469c0be..ece465d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.md CONTRIBUTING.md LICENSE VERSION +recursive-include aip_site/vendor * recursive-include aip_site/support * global-exclude *.py[co] global-exclude __pycache__ diff --git a/aip_site/__init__.py b/aip_site/__init__.py index e69de29..baf5958 100644 --- a/aip_site/__init__.py +++ b/aip_site/__init__.py @@ -0,0 +1,11 @@ +import sys +import os + +vendor_dir = os.path.join(os.path.dirname(__file__), 'vendor') + +for item in os.listdir(vendor_dir): + path = os.path.join(vendor_dir, item) + if not os.path.isdir(path): + continue + if path not in sys.path: + sys.path.insert(0, path) diff --git a/aip_site/vendor/__init__.py b/aip_site/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aip_site/vendor/pyscss/LICENSE b/aip_site/vendor/pyscss/LICENSE new file mode 100644 index 0000000..fd50e09 --- /dev/null +++ b/aip_site/vendor/pyscss/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2011, 2012 German M. Bravo (Kronuz) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/aip_site/vendor/pyscss/__init__.py b/aip_site/vendor/pyscss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aip_site/vendor/pyscss/bin/less2scss b/aip_site/vendor/pyscss/bin/less2scss new file mode 100755 index 0000000..2ac8ecf --- /dev/null +++ b/aip_site/vendor/pyscss/bin/less2scss @@ -0,0 +1,7 @@ +#!/opt/homebrew/opt/python@3.12/bin/python3.12 +import sys +from scss.less2scss import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/aip_site/vendor/pyscss/bin/pyscss b/aip_site/vendor/pyscss/bin/pyscss new file mode 100755 index 0000000..b19b1cf --- /dev/null +++ b/aip_site/vendor/pyscss/bin/pyscss @@ -0,0 +1,7 @@ +#!/opt/homebrew/opt/python@3.12/bin/python3.12 +import sys +from scss.tool import main +if __name__ == '__main__': + if sys.argv[0].endswith('.exe'): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) diff --git a/aip_site/vendor/pyscss/scss/__init__.py b/aip_site/vendor/pyscss/scss/__init__.py new file mode 100644 index 0000000..be054ee --- /dev/null +++ b/aip_site/vendor/pyscss/scss/__init__.py @@ -0,0 +1,63 @@ +#-*- coding: utf-8 -*- +""" +pyScss, a Scss compiler for Python + +@author German M. Bravo (Kronuz) +@version 1.2.0 alpha +@see https://github.com/Kronuz/pyScss +@copyright (c) 2012-2013 German M. Bravo (Kronuz) +@license MIT License + http://www.opensource.org/licenses/mit-license.php + +pyScss compiles Scss, a superset of CSS that is more powerful, elegant and +easier to maintain than plain-vanilla CSS. The library acts as a CSS source code +preprocesor which allows you to use variables, nested rules, mixins, andhave +inheritance of rules, all with a CSS-compatible syntax which the preprocessor +then compiles to standard CSS. + +Scss, as an extension of CSS, helps keep large stylesheets well-organized. It +borrows concepts and functionality from projects such as OOCSS and other similar +frameworks like as Sass. It's build on top of the original PHP xCSS codebase +structure but it's been completely rewritten, many bugs have been fixed and it +has been extensively extended to support almost the full range of Sass' Scss +syntax and functionality. + +Bits of code in pyScss come from various projects: +Compass: + (c) 2009 Christopher M. Eppstein + http://compass-style.org/ +Sass: + (c) 2006-2009 Hampton Catlin and Nathan Weizenbaum + http://sass-lang.com/ +xCSS: + (c) 2010 Anton Pawlik + http://xcss.antpaw.org/docs/ + +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +from scss.scss_meta import BUILD_INFO, PROJECT, VERSION, REVISION, URL, AUTHOR, AUTHOR_EMAIL, LICENSE + +__project__ = PROJECT +__version__ = VERSION +__author__ = AUTHOR + ' <' + AUTHOR_EMAIL + '>' +__license__ = LICENSE + + +import logging + +log = logging.getLogger(__name__) + + +# Helpful re-exports +from scss.compiler import Compiler + +# Backwards compatibility +from scss.legacy import Scss +# TODO surely there are others. what do our own django docs say...? + + +__all__ = ['Compiler'] diff --git a/aip_site/vendor/pyscss/scss/__main__.py b/aip_site/vendor/pyscss/scss/__main__.py new file mode 100644 index 0000000..792ead7 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/__main__.py @@ -0,0 +1,3 @@ +import scss.tool + +scss.tool.main() diff --git a/aip_site/vendor/pyscss/scss/ast.py b/aip_site/vendor/pyscss/scss/ast.py new file mode 100644 index 0000000..16a4518 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/ast.py @@ -0,0 +1,565 @@ +"""Syntax tree for parsed Sass expressions. + +The overall structure for a Sass file uses a different kind of AST; have a look +at :mod:`scss.blockast`. +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from functools import partial +import logging +import operator + +try: + from collections import OrderedDict +except ImportError: + # Backport + from ordereddict import OrderedDict + +import six + +from scss.cssdefs import COLOR_NAMES +from scss.cssdefs import is_builtin_css_function +from scss.types import Boolean +from scss.types import Color +from scss.types import Function +from scss.types import List +from scss.types import Map +from scss.types import Null +from scss.types import String +from scss.types import Undefined +from scss.types import Url +from scss.types import Value +from scss.util import normalize_var + + +log = logging.getLogger(__name__) + + +class Expression(object): + def __repr__(self): + return '<%s()>' % (self.__class__.__name__) + + def evaluate(self, calculator, divide=False): + """Evaluate this AST node, and return a Sass value. + + `divide` indicates whether a descendant node representing a division + should be forcibly treated as a division. See the commentary in + `BinaryOp`. + """ + raise NotImplementedError + + +class Parentheses(Expression): + """An expression of the form `(foo)`. + + Only exists to force a slash to be interpreted as division when contained + within parentheses. + """ + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.contents)) + + def __init__(self, contents): + self.contents = contents + + def evaluate(self, calculator, divide=False): + return self.contents.evaluate(calculator, divide=True) + + +class UnaryOp(Expression): + def __repr__(self): + return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.operand)) + + def __init__(self, op, operand): + self.op = op + self.operand = operand + + def evaluate(self, calculator, divide=False): + return self.op(self.operand.evaluate(calculator, divide=True)) + + +class BinaryOp(Expression): + OPERATORS = { + operator.lt: '<', + operator.gt: '>', + operator.le: '<=', + operator.ge: '>=', + operator.eq: '==', + operator.eq: '!=', + operator.add: '+', + operator.sub: '-', + operator.mul: '*', + operator.truediv: '/', + operator.mod: '%', + } + + def __repr__(self): + return '<%s(%s, %s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.left), repr(self.right)) + + def __init__(self, op, left, right): + self.op = op + self.left = left + self.right = right + + def evaluate(self, calculator, divide=False): + left = self.left.evaluate(calculator, divide=True) + right = self.right.evaluate(calculator, divide=True) + + # Determine whether to actually evaluate, or just print the operator + # literally. + literal = False + + # If either operand starts with an interpolation, treat the whole + # shebang as literal. + if any(isinstance(operand, Interpolation) and operand.parts[0] == '' + for operand in (self.left, self.right)): + literal = True + + # Special handling of division: treat it as a literal slash if both + # operands are literals, there are no parentheses, and this isn't part + # of a bigger expression. + # The first condition is covered by the type check. The other two are + # covered by the `divide` argument: other nodes that perform arithmetic + # will pass in True, indicating that this should always be a division. + elif ( + self.op is operator.truediv + and not divide + and isinstance(self.left, Literal) + and isinstance(self.right, Literal) + ): + literal = True + + if literal: + # TODO we don't currently preserve the spacing, whereas Sass + # remembers whether there was space on either side + op = " {0} ".format(self.OPERATORS[self.op]) + return String.unquoted(left.render() + op + right.render()) + + return self.op(left, right) + + +class AnyOp(Expression): + def __repr__(self): + return '<%s(*%s)>' % (self.__class__.__name__, repr(self.operands)) + + def __init__(self, *operands): + self.operands = operands + + def evaluate(self, calculator, divide=False): + for operand in self.operands: + value = operand.evaluate(calculator, divide=True) + if value: + return value + return value + + +class AllOp(Expression): + def __repr__(self): + return '<%s(*%s)>' % (self.__class__.__name__, repr(self.operands)) + + def __init__(self, *operands): + self.operands = operands + + def evaluate(self, calculator, divide=False): + for operand in self.operands: + value = operand.evaluate(calculator, divide=True) + if not value: + return value + return value + + +class NotOp(Expression): + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.operand)) + + def __init__(self, operand): + self.operand = operand + + def evaluate(self, calculator, divide=False): + operand = self.operand.evaluate(calculator, divide=True) + return Boolean(not(operand)) + + +class CallOp(Expression): + def __repr__(self): + return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.func_name), repr(self.argspec)) + + def __init__(self, func_name, argspec): + self.func_name = func_name + self.argspec = argspec + + def evaluate(self, calculator, divide=False): + # TODO bake this into the context and options "dicts", plus library + func_name = normalize_var(self.func_name) + + argspec_node = self.argspec + + # Turn the pairs of arg tuples into *args and **kwargs + # TODO unclear whether this is correct -- how does arg, kwarg, arg + # work? + args, kwargs = argspec_node.evaluate_call_args(calculator) + argspec_len = len(args) + len(kwargs) + + # Translate variable names to Python identifiers + # TODO what about duplicate kw names? should this happen in argspec? + # how does that affect mixins? + kwargs = dict( + (key.lstrip('$').replace('-', '_'), value) + for key, value in kwargs.items()) + + # TODO merge this with the library + funct = None + try: + funct = calculator.namespace.function(func_name, argspec_len) + except KeyError: + try: + # DEVIATION: Fall back to single parameter + funct = calculator.namespace.function(func_name, 1) + args = [List(args, use_comma=True)] + except KeyError: + if not is_builtin_css_function(func_name): + log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True}) + + if funct: + if getattr(funct, '_pyscss_needs_namespace', False): + # @functions and some Python functions take the namespace as an + # extra first argument + ret = funct(calculator.namespace, *args, **kwargs) + else: + ret = funct(*args, **kwargs) + if not isinstance(ret, Value): + raise TypeError("Expected Sass type as return value, got %r" % (ret,)) + return ret + + # No matching function found, so render the computed values as a CSS + # function call. Slurpy arguments are expanded and named arguments are + # unsupported. + if kwargs: + raise TypeError("The CSS function %s doesn't support keyword arguments." % (func_name,)) + + # TODO another candidate for a "function call" sass type + rendered_args = [arg.render() for arg in args] + + return String( + "%s(%s)" % (func_name, ", ".join(rendered_args)), + quotes=None) + + +# TODO this class should delegate the unescaping to the type, rather than +# burying it in the parser +class Interpolation(Expression): + """A string that may contain any number of interpolations: + + foo#{...}bar#{...}baz + """ + def __init__(self, parts, quotes=None, type=String, **kwargs): + self.parts = parts + self.quotes = quotes + self.type = type + self.kwargs = kwargs + + def __repr__(self): + repr_parts = [] + for i, part in enumerate(self.parts): + if i % 2 == 0: + if part: + repr_parts.append(repr(part)) + else: + repr_parts.append('#{' + repr(part) + '}') + + return "<{0} {1}>".format(type(self).__name__, " ".join(repr_parts)) + + @classmethod + def maybe(cls, parts, quotes=None, type=String, **kwargs): + """Returns an interpolation if there are multiple parts, otherwise a + plain Literal. This keeps the AST somewhat simpler, but also is the + only way `Literal.from_bareword` gets called. + """ + if len(parts) > 1: + return cls(parts, quotes=quotes, type=type, **kwargs) + + if quotes is None and type is String: + return Literal.from_bareword(parts[0]) + + return Literal(type(parts[0], quotes=quotes, **kwargs)) + + def evaluate(self, calculator, divide=False): + result = [] + for i, part in enumerate(self.parts): + if i % 2 == 0: + # First part and other odd parts are literal string + result.append(part) + else: + # Interspersed (even) parts are nodes + value = part.evaluate(calculator, divide) + # TODO need to know whether to pass `compress` here + result.append(value.render_interpolated()) + + return self.type(''.join(result), quotes=self.quotes, **self.kwargs) + + + +class Literal(Expression): + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.value)) + + def __init__(self, value): + self.value = value + + @classmethod + def from_bareword(cls, word): + if word in COLOR_NAMES: + value = Color.from_name(word) + elif word == 'null': + value = Null() + elif word == 'undefined': + value = Undefined() + elif word == 'true': + value = Boolean(True) + elif word == 'false': + value = Boolean(False) + else: + value = String(word, quotes=None) + + return cls(value) + + def evaluate(self, calculator, divide=False): + if (isinstance(self.value, Undefined) and + calculator.undefined_variables_fatal): + raise SyntaxError("Undefined literal.") + + return self.value + + +class Variable(Expression): + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.name)) + + def __init__(self, name): + self.name = name + + def evaluate(self, calculator, divide=False): + try: + value = calculator.namespace.variable(self.name) + except KeyError: + if calculator.undefined_variables_fatal: + raise SyntaxError("Undefined variable: '%s'." % self.name) + else: + log.error("Undefined variable '%s'", self.name, extra={'stack': True}) + return Undefined() + else: + if isinstance(value, six.string_types): + log.warn( + "Expected a Sass type for the value of {0}, " + "but found a string expression: {1!r}" + .format(self.name, value) + ) + evald = calculator.evaluate_expression(value) + if evald is not None: + return evald + return value + + +class ListLiteral(Expression): + def __repr__(self): + return '<%s(%s, comma=%s)>' % (self.__class__.__name__, repr(self.items), repr(self.comma)) + + def __init__(self, items, comma=True): + self.items = items + self.comma = comma + + def evaluate(self, calculator, divide=False): + items = [item.evaluate(calculator, divide=divide) for item in self.items] + + # Whether this is a "plain" literal matters for null removal: nulls are + # left alone if this is a completely vanilla CSS property + literal = True + if divide: + # TODO sort of overloading "divide" here... rename i think + literal = False + elif not all(isinstance(item, Literal) for item in self.items): + literal = False + + return List(items, use_comma=self.comma, literal=literal) + + +class MapLiteral(Expression): + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.pairs)) + + def __init__(self, pairs): + self.pairs = tuple((var, value) for var, value in pairs if value is not None) + + def evaluate(self, calculator, divide=False): + scss_pairs = [] + for key, value in self.pairs: + scss_pairs.append(( + key.evaluate(calculator), + value.evaluate(calculator), + )) + + return Map(scss_pairs) + + +class ArgspecLiteral(Expression): + """Contains pairs of argument names and values, as parsed from a function + definition or function call. + + Note that the semantics are somewhat ambiguous. Consider parsing: + + $foo, $bar: 3 + + If this appeared in a function call, $foo would refer to a value; if it + appeared in a function definition, $foo would refer to an existing + variable. This it's up to the caller to use the right iteration function. + """ + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, repr(self.argpairs)) + + def __init__(self, argpairs, slurp=None): + # argpairs is a list of 2-tuples, parsed as though this were a function + # call, so (variable name as string or None, default value as AST + # node). + # slurp is the name of a variable to receive slurpy arguments. + self.argpairs = tuple(argpairs) + if slurp is all: + # DEVIATION: special syntax to allow injecting arbitrary arguments + # from the caller to the callee + self.inject = True + self.slurp = None + elif slurp: + self.inject = False + self.slurp = Variable(slurp) + else: + self.inject = False + self.slurp = None + + def iter_list_argspec(self): + yield None, ListLiteral(zip(*self.argpairs)[1]) + + def iter_def_argspec(self): + """Interpreting this literal as a function definition, yields pairs of + (variable name as a string, default value as an AST node or None). + """ + started_kwargs = False + seen_vars = set() + + for var, value in self.argpairs: + if var is None: + # value is actually the name + var = value + value = None + + if started_kwargs: + raise SyntaxError( + "Required argument %r must precede optional arguments" + % (var.name,)) + + else: + started_kwargs = True + + if not isinstance(var, Variable): + raise SyntaxError("Expected variable name, got %r" % (var,)) + + if var.name in seen_vars: + raise SyntaxError("Duplicate argument %r" % (var.name,)) + seen_vars.add(var.name) + + yield var.name, value + + def evaluate_call_args(self, calculator): + """Interpreting this literal as a function call, return a 2-tuple of + ``(args, kwargs)``. + """ + args = [] + kwargs = OrderedDict() # Sass kwargs preserve order + for var_node, value_node in self.argpairs: + value = value_node.evaluate(calculator, divide=True) + if var_node is None: + # Positional + args.append(value) + else: + # Named + if not isinstance(var_node, Variable): + raise TypeError( + "Expected variable name, got {0!r}".format(var_node)) + kwargs[var_node.name] = value + + # Slurpy arguments go on the end of the args + if self.slurp: + args.extend(self.slurp.evaluate(calculator, divide=True)) + + return args, kwargs + + +class FunctionLiteral(Expression): + """Wraps an existing AST node in a literal (unevaluated) function call.""" + def __init__(self, child, function_name): + self.child = child + self.function_name = function_name + + def evaluate(self, calculator, divide=False): + child = self.child.evaluate(calculator, divide) + if isinstance(child, String): + contents = child.value + quotes = child.quotes + else: + # TODO compress + contents = child.render() + quotes = None + + # TODO unclear if this is the right place for this logic, or if it + # should go in the Function constructor, or should be passed in + # explicitly by the grammar, or even if Url should go away entirely + if self.function_name == "url": + return Url(contents, quotes=quotes) + else: + return Function(contents, self.function_name, quotes=quotes) + + +class AlphaFunctionLiteral(Expression): + """Wraps an existing AST node in a literal (unevaluated) function call, + prepending "opacity=" to the contents. + """ + def __init__(self, child): + self.child = child + + def evaluate(self, calculator, divide=False): + child = self.child.evaluate(calculator, divide) + if isinstance(child, String): + contents = child.value + else: + # TODO compress + contents = child.render() + return Function('opacity=' + contents, 'alpha', quotes=None) + + +class TernaryOp(Expression): + """Sass implements this with a function: + + prop: if(condition, true-value, false-value); + + However, the second and third arguments are guaranteed not to be evaluated + unless necessary. Functions always receive evaluated arguments, so this is + a syntactic construct in disguise. + """ + def __repr__(self): + return '<%s(%r, %r, %r)>' % ( + self.__class__.__name__, + self.condition, + self.true_expression, + self.false_expression, + ) + + def __init__(self, list_literal): + args = list_literal.items + if len(args) != 3: + raise SyntaxError("if() must have exactly 3 arguments") + self.condition, self.true_expression, self.false_expression = args + + def evaluate(self, calculator, divide=False): + if self.condition.evaluate(calculator, divide=True): + return self.true_expression.evaluate(calculator, divide=True) + else: + return self.false_expression.evaluate(calculator, divide=True) diff --git a/aip_site/vendor/pyscss/scss/calculator.py b/aip_site/vendor/pyscss/scss/calculator.py new file mode 100644 index 0000000..c6c8fb1 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/calculator.py @@ -0,0 +1,196 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import sys +import logging +from warnings import warn + +import six + +from scss.ast import Literal +from scss.cssdefs import _expr_glob_re, _interpolate_re +from scss.errors import SassError, SassEvaluationError, SassParseError +from scss.grammar.expression import SassExpression, SassExpressionScanner +from scss.rule import Namespace +from scss.types import String +from scss.types import Value +from scss.util import dequote + + +log = logging.getLogger(__name__) + + +class Calculator(object): + """Expression evaluator.""" + + ast_cache = {} + + def __init__( + self, namespace=None, + ignore_parse_errors=False, + undefined_variables_fatal=True, + ): + if namespace is None: + self.namespace = Namespace() + else: + self.namespace = namespace + + self.ignore_parse_errors = ignore_parse_errors + self.undefined_variables_fatal = undefined_variables_fatal + + def _pound_substitute(self, result): + expr = result.group(1) + value = self.evaluate_expression(expr) + + if value is None: + return self.apply_vars(expr) + elif value.is_null: + return "" + else: + return dequote(value.render()) + + def do_glob_math(self, cont): + """Performs #{}-interpolation. The result is always treated as a fixed + syntactic unit and will not be re-evaluated. + """ + # TODO that's a lie! this should be in the parser for most cases. + if not isinstance(cont, six.string_types): + warn(FutureWarning( + "do_glob_math was passed a non-string {0!r} " + "-- this will no longer be supported in pyScss 2.0" + .format(cont) + )) + cont = six.text_type(cont) + if '#{' not in cont: + return cont + cont = _expr_glob_re.sub(self._pound_substitute, cont) + return cont + + def apply_vars(self, cont): + # TODO this is very complicated. it should go away once everything + # valid is actually parseable. + if isinstance(cont, six.string_types) and '$' in cont: + try: + # Optimization: the full cont is a variable in the context, + cont = self.namespace.variable(cont) + except KeyError: + # Interpolate variables: + def _av(m): + v = None + n = m.group(2) + try: + v = self.namespace.variable(n) + except KeyError: + if self.undefined_variables_fatal: + raise SyntaxError("Undefined variable: '%s'." % n) + else: + log.error("Undefined variable '%s'", n, extra={'stack': True}) + return n + else: + if v: + if not isinstance(v, Value): + raise TypeError( + "Somehow got a variable {0!r} " + "with a non-Sass value: {1!r}" + .format(n, v) + ) + v = v.render() + # TODO this used to test for _dequote + if m.group(1): + v = dequote(v) + else: + v = m.group(0) + return v + + cont = _interpolate_re.sub(_av, cont) + + else: + # Variable succeeded, so we need to render it + cont = cont.render() + # TODO this is surprising and shouldn't be here + cont = self.do_glob_math(cont) + return cont + + def calculate(self, expression, divide=False): + result = self.evaluate_expression(expression, divide=divide) + + if result is None: + return String.unquoted(self.apply_vars(expression)) + + return result + + # TODO only used by magic-import...? + def interpolate(self, var): + value = self.namespace.variable(var) + if var != value and isinstance(value, six.string_types): + _vi = self.evaluate_expression(value) + if _vi is not None: + value = _vi + return value + + def evaluate_expression(self, expr, divide=False): + try: + ast = self.parse_expression(expr) + except SassError as e: + if self.ignore_parse_errors: + return None + raise + + try: + return ast.evaluate(self, divide=divide) + except Exception as e: + six.reraise(SassEvaluationError, SassEvaluationError(e, expression=expr), sys.exc_info()[2]) + + def parse_expression(self, expr, target='goal'): + if isinstance(expr, six.text_type): + # OK + pass + elif isinstance(expr, six.binary_type): + # Dubious + warn(FutureWarning( + "parse_expression was passed binary data {0!r} " + "-- this will no longer be supported in pyScss 2.0" + .format(expr) + )) + # Don't guess an encoding; you reap what you sow + expr = six.text_type(expr) + else: + raise TypeError("Expected string, got %r" % (expr,)) + + key = (target, expr) + if key in self.ast_cache: + return self.ast_cache[key] + + try: + parser = SassExpression(SassExpressionScanner(expr)) + ast = getattr(parser, target)() + except SyntaxError as e: + raise SassParseError(e, expression=expr, expression_pos=parser._char_pos) + + self.ast_cache[key] = ast + return ast + + def parse_interpolations(self, string): + """Parse a string for interpolations, but don't treat anything else as + Sass syntax. Returns an AST node. + """ + # Shortcut: if there are no #s in the string in the first place, it + # must not have any interpolations, right? + if '#' not in string: + return Literal(String.unquoted(string)) + return self.parse_expression(string, 'goal_interpolated_literal') + + def parse_vars_and_interpolations(self, string): + """Parse a string for variables and interpolations, but don't treat + anything else as Sass syntax. Returns an AST node. + """ + # Shortcut: if there are no #s or $s in the string in the first place, + # it must not have anything of interest. + if '#' not in string and '$' not in string: + return Literal(String.unquoted(string)) + return self.parse_expression( + string, 'goal_interpolated_literal_with_vars') + + +__all__ = ('Calculator',) diff --git a/aip_site/vendor/pyscss/scss/compiler.py b/aip_site/vendor/pyscss/scss/compiler.py new file mode 100644 index 0000000..2fb164b --- /dev/null +++ b/aip_site/vendor/pyscss/scss/compiler.py @@ -0,0 +1,1502 @@ +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 logging +from pathlib import Path +import re +import sys +import warnings + +try: + from collections import OrderedDict +except ImportError: + # Backport + from ordereddict import OrderedDict + +import six + +from scss.calculator import Calculator +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.errors import SassBaseError +from scss.errors import SassImportError +from scss.extension import Extension +from scss.extension.core import CoreExtension +from scss.extension import NamespaceAdapterExtension +from scss.grammar import locate_blocks +from scss.rule import BlockAtRuleHeader +from scss.rule import Namespace +from scss.rule import RuleAncestry +from scss.rule import SassRule +from scss.rule import UnparsedBlock +from scss.selector import Selector +from scss.source import SourceFile +from scss.types import Arglist +from scss.types import List +from scss.types import Null +from scss.types import Number +from scss.types import String +from scss.types import Undefined +from scss.types import Url +from scss.util import normalize_var # TODO put in... namespace maybe? + + +# TODO should mention logging for the programmatic interface in the +# documentation +# TODO or have a little helper (or compiler setting) to turn it on +log = logging.getLogger(__name__) + + +_xcss_extends_re = re.compile(r'\s+extends\s+') + + +class OutputStyle(Enum): + nested = () + compact = () + compressed = () + expanded = () + + legacy = () # ??? + + +class SassDeprecationWarning(UserWarning): + # Note: DO NOT inherit from DeprecationWarning; it's turned off by default + # in 2.7 and later! + pass + + +def warn_deprecated(rule, message): + warnings.warn( + "{0} (at {1})".format(message, rule.file_and_line), + SassDeprecationWarning, + stacklevel=2, + ) + + +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=Path(), search_path=(), + namespace=None, extensions=(CoreExtension,), + import_static_css=False, + output_style='nested', generate_source_map=False, + live_errors=False, warn_unused_imports=False, + ignore_parse_errors=False, + loops_have_own_scopes=True, + undefined_variables_fatal=True, + 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. + :type root: :class:`pathlib.Path` + :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. + :type search_path: list of strings, :class:`pathlib.Path` objects, or + something that implements a similar interface (useful for custom + pseudo filesystems) + """ + # TODO perhaps polite to automatically cast any string paths to Path? + # but have to be careful since the api explicitly allows dummy objects. + if root is None: + self.root = None + else: + self.root = root.resolve() + + self.search_path = tuple( + self.normalize_path(path) + for path in search_path + ) + + self.extensions = [] + if namespace is not None: + self.extensions.append(NamespaceAdapterExtension(namespace)) + for extension in extensions: + if isinstance(extension, Extension): + self.extensions.append(extension) + elif (isinstance(extension, type) and + issubclass(extension, Extension)): + self.extensions.append(extension()) + elif isinstance(extension, Namespace): + self.extensions.append( + NamespaceAdapterExtension(extension)) + else: + raise TypeError( + "Expected an Extension or Namespace, got: {0!r}" + .format(extension) + ) + + if import_static_css: + self.dynamic_extensions = ('.scss', '.sass', '.css') + self.static_extensions = () + else: + self.dynamic_extensions = ('.scss', '.sass') + self.static_extensions = ('.css',) + + 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.ignore_parse_errors = ignore_parse_errors + self.loops_have_own_scopes = loops_have_own_scopes + self.undefined_variables_fatal = undefined_variables_fatal + self.super_selector = super_selector + + def normalize_path(self, path): + if isinstance(path, six.string_types): + path = Path(path) + if path.is_absolute(): + return path + if self.root is None: + raise IOError("Can't make absolute path when root is None") + return self.root / path + + def make_compilation(self): + return Compilation(self) + + def call_and_catch_errors(self, f, *args, **kwargs): + """Call the given function with the given arguments. If it succeeds, + return its return value. If it raises a :class:`scss.errors.SassError` + and `live_errors` is turned on, return CSS containing a traceback and + error message. + """ + try: + return f(*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 compile(self, *filenames): + # TODO this doesn't spit out the compilation itself, so if you want to + # get something out besides just the output, you have to copy this + # method. that sucks. + # TODO i think the right thing is to get all the constructors out of + # SourceFile, since it's really the compiler that knows the import + # paths and should be consulted about this. reconsider all this (but + # preserve it for now, SIGH) once importers are a thing + compilation = self.make_compilation() + for filename in filenames: + # TODO maybe SourceFile should not be exposed to the end user, and + # instead Compilation should have methods for add_string etc. that + # can call normalize_path. + # TODO it's not possible to inject custom files into the + # /compiler/ as persistent across compiles, nor to provide "fake" + # imports. do we want the former? is the latter better suited to + # an extension? + source = SourceFile.from_filename(self.normalize_path(filename)) + compilation.add_source(source) + return self.call_and_catch_errors(compilation.run) + + def compile_sources(self, *sources): + # TODO this api is not the best please don't use it. this all needs to + # be vastly simplified, still, somehow. + compilation = self.make_compilation() + for source in sources: + compilation.add_source(source) + return self.call_and_catch_errors(compilation.run) + + def compile_string(self, string): + source = SourceFile.from_string(string) + compilation = self.make_compilation() + compilation.add_source(source) + return self.call_and_catch_errors(compilation.run) + + +def compile_file(filename, compiler_class=Compiler, **kwargs): + """Compile a single file (provided as a :class:`pathlib.Path`), and return + a string of CSS. + + Keyword arguments are passed along to the underlying `Compiler`. + + Note that the search path is set to the file's containing directory by + default, unless you explicitly pass a ``search_path`` kwarg. + + :param filename: Path to the file to compile. + :type filename: str, bytes, or :class:`pathlib.Path` + """ + filename = Path(filename) + if 'search_path' not in kwargs: + kwargs['search_path'] = [filename.parent.resolve()] + + compiler = compiler_class(**kwargs) + return compiler.compile(filename) + + +def compile_string(string, compiler_class=Compiler, **kwargs): + """Compile a single string, and return a string of CSS. + + Keyword arguments are passed along to the underlying `Compiler`. + """ + compiler = compiler_class(**kwargs) + return compiler.compile_string(string) + + +class Compilation(object): + """A single run of a compiler.""" + def __init__(self, compiler): + self.compiler = compiler + self.ignore_parse_errors = compiler.ignore_parse_errors + + # TODO this needs a write barrier, so assignment can't overwrite what's + # in the original namespaces + # TODO or maybe the extensions themselves should take care of that, so + # it IS possible to overwrite from within sass, but only per-instance? + self.root_namespace = Namespace.derive_from(*( + ext.namespace for ext in compiler.extensions + if ext.namespace + )) + + self.sources = [] + self.source_index = {} + self.dependency_map = defaultdict(frozenset) + self.rules = [] + + def should_scope_loop_in_rule(self, rule): + """Return True iff a looping construct (@each, @for, @while, @if) + should get its own scope, as is standard Sass behavior. + """ + return rule.legacy_compiler_options.get( + 'control_scoping', self.compiler.loops_have_own_scopes) + + def add_source(self, source): + if source.key in self.source_index: + return self.source_index[source.key] + self.sources.append(source) + self.source_index[source.key] = source + return source + + def run(self): + # Any @import will add the source file to self.sources and infect this + # list, so make a quick copy to insulate against that + # TODO maybe @import should just not do that? + for source_file in list(self.sources): + rule = SassRule( + source_file=source_file, + lineno=1, + + unparsed_contents=source_file.contents, + namespace=self.root_namespace, + ) + self.rules.append(rule) + self.manage_children(rule, scope=None) + self._warn_unused_imports(rule) + + # Run through all the rules and apply @extends in a separate pass + self.rules = self.apply_extends(self.rules) + + output, total_selectors = self.create_css(self.rules) + if total_selectors > 65534: + log.warning("Maximum number of supported selectors in Internet Explorer (65534) exceeded!") + + return output + + 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 + + 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) + + def _make_calculator(self, namespace): + return Calculator( + namespace, + ignore_parse_errors=self.ignore_parse_errors, + undefined_variables_fatal=self.compiler.undefined_variables_fatal, + ) + + # @print_timing(4) + def manage_children(self, rule, scope): + try: + self._manage_children_impl(rule, scope) + except SassBaseError as e: + e.add_rule(rule) + raise + except Exception as e: + raise SassError(e, rule=rule) + + def _manage_children_impl(self, rule, scope): + calculator = self._make_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: + # TODO particularly wild idea: allow extensions to handle + # unrecognized blocks, and get the pyscss stuff out of the + # core? even move the core stuff into the core extension? + 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 + self.ignore_parse_errors = setting + log.info("Debug mode is %s", 'On' if self.ignore_parse_errors 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) + rule.extends_selectors.extend(Selector.parse_many(selectors)) + + 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': + warn_deprecated( + rule, + "The 'compress' @option is deprecated. " + "Please use 'style' instead." + ) + key = 'style' + value = 'compressed' if value else 'legacy' + + if key in ('short_colors', 'reverse_colors'): + warn_deprecated( + rule, + "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 = '' + + 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 = OrderedDict(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 = self._make_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, try the default argument. Using callee_calculator means + # that default values of arguments can refer to earlier + # arguments' values; yes, that is how Sass works. + 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 + # TODO should preserve the order of extra kwargs + sass_kwargs = [] + for key, value in kwargs.items(): + sass_kwargs.append((String(key[1:]), value)) + callee_namespace.set_variable( + callee_argspec.slurp.name, + Arglist(args, sass_kwargs)) + args = [] + kwargs = {} + 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.source_file] + 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 = OrderedDict( + (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, + ) + # TODO supposed to throw an error if there's a slurpy arg + # but keywords() is never called on it + try: + self.manage_children(_rule, scope) + except SassReturn as e: + return e.retval + else: + return Null() + __call._pyscss_needs_namespace = True + 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 = self._make_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 + """ + # TODO it would be neat to opt into warning that you're using + # values/functions from a file you didn't explicitly import + # TODO base-level directives, like @mixin or @charset, aren't allowed + # to be @imported into a nested block + # TODO i'm not sure we disallow them nested in the first place + # TODO @import is disallowed within mixins, control directives + # TODO @import doesn't take a block -- that's probably an issue with a + # lot of our directives + + # TODO if there's any #{}-interpolation in the AST, this should become + # a CSS import (though in practice Ruby only even evaluates it in url() + # -- in a string it's literal!) + + sass_paths = calculator.evaluate_expression(block.argument) + css_imports = [] + + for sass_path in sass_paths: + # These are the rules for when an @import is interpreted as a CSS + # import: + if ( + # If it's a url() + isinstance(sass_path, Url) or + # If it's not a string (including `"foo" screen`, a List) + not isinstance(sass_path, String) or + # If the filename begins with an http protocol + sass_path.value.startswith(('http://', 'https://')) or + # If the filename ends with .css + sass_path.value.endswith(self.compiler.static_extensions)): + css_imports.append(sass_path.render(compress=False)) + continue + + # Should be left with a plain String + name = sass_path.value + + source = None + for extension in self.compiler.extensions: + source = extension.handle_import(name, self, rule) + if source: + break + else: + # Didn't find anything! + raise SassImportError(name, self.compiler, rule=rule) + + source = self.add_source(source) + + if rule.namespace.has_import(source): + # If already imported in this scope, skip + # TODO this might not be right -- consider if you @import a + # file at top level, then @import it inside a selector block! + continue + + _rule = SassRule( + source_file=source, + lineno=block.lineno, + unparsed_contents=source.contents, + + # rule + legacy_compiler_options=rule.legacy_compiler_options, + options=rule.options, + properties=rule.properties, + extends_selectors=rule.extends_selectors, + ancestry=rule.ancestry, + namespace=rule.namespace, + ) + rule.namespace.add_import(source, rule) + self.manage_children(_rule, scope) + + # Create a new @import rule for each import determined to be CSS + for import_ in css_imports: + # TODO this seems extremely janky (surely we should create an + # actual new Rule), but the CSS rendering doesn't understand how to + # print rules without blocks + # TODO if this ever creates a new Rule, shuffle stuff around so + # this is still hoisted to the top + rule.properties.append(('@import ' + import_, None)) + + # @print_timing(10) + def _at_if(self, calculator, rule, scope, block): + """ + Implements @if and @else if + """ + # "@if" indicates whether any kind of `if` since the last `@else` has + # succeeded, in which case `@else if` should be skipped + if block.directive != '@if': + if '@if' not in rule.options: + raise SyntaxError("@else with no @if (%s)" % (rule.file_and_line,)) + if rule.options['@if']: + # Last @if succeeded; stop here + return + + condition = calculator.calculate(block.argument) + if condition: + inner_rule = rule.copy() + inner_rule.unparsed_contents = block.unparsed_contents + if not self.should_scope_loop_in_rule(inner_rule): + # DEVIATION: Allow not creating a new namespace + inner_rule.namespace = rule.namespace + self.manage_children(inner_rule, scope) + rule.options['@if'] = condition + _at_else_if = _at_if + + # @print_timing(10) + def _at_else(self, calculator, rule, scope, block): + """ + Implements @else + """ + if '@if' not in rule.options: + log.error("@else with no @if (%s)", rule.file_and_line) + val = rule.options.pop('@if', True) + if not val: + inner_rule = rule.copy() + inner_rule.unparsed_contents = block.unparsed_contents + inner_rule.namespace = rule.namespace # DEVIATION: Commenting this line gives the Sass bahavior + inner_rule.unparsed_contents = block.unparsed_contents + self.manage_children(inner_rule, scope) + + # @print_timing(10) + def _at_for(self, calculator, rule, scope, block): + """ + Implements @for + """ + var, _, name = block.argument.partition(' from ') + frm, _, through = name.partition(' through ') + if through: + inclusive = True + else: + inclusive = False + frm, _, through = frm.partition(' to ') + frm = calculator.calculate(frm) + through = calculator.calculate(through) + try: + frm = int(float(frm)) + through = int(float(through)) + except ValueError: + return + + if frm > through: + # DEVIATION: allow reversed '@for .. from .. through' (same as enumerate() and range()) + frm, through = through, frm + rev = reversed + else: + rev = lambda x: x + var = var.strip() + var = calculator.do_glob_math(var) + var = normalize_var(var) + + inner_rule = rule.copy() + inner_rule.unparsed_contents = block.unparsed_contents + if not self.should_scope_loop_in_rule(inner_rule): + # DEVIATION: Allow not creating a new namespace + inner_rule.namespace = rule.namespace + + if inclusive: + through += 1 + for i in rev(range(frm, through)): + inner_rule.namespace.set_variable(var, Number(i)) + self.manage_children(inner_rule, scope) + + # @print_timing(10) + def _at_each(self, calculator, rule, scope, block): + """ + Implements @each + """ + varstring, _, valuestring = block.argument.partition(' in ') + values = calculator.calculate(valuestring) + if not values: + return + + varlist = [ + normalize_var(calculator.do_glob_math(var.strip())) + # TODO use list parsing here + for var in varstring.split(",") + ] + + # `@each $foo, in $bar` unpacks, but `@each $foo in $bar` does not! + unpack = len(varlist) > 1 + if not varlist[-1]: + varlist.pop() + + inner_rule = rule.copy() + inner_rule.unparsed_contents = block.unparsed_contents + if not self.should_scope_loop_in_rule(inner_rule): + # DEVIATION: Allow not creating a new namespace + inner_rule.namespace = rule.namespace + + for v in List.from_maybe(values): + if unpack: + v = List.from_maybe(v) + for i, var in enumerate(varlist): + if i >= len(v): + value = Null() + else: + value = v[i] + inner_rule.namespace.set_variable(var, value) + else: + inner_rule.namespace.set_variable(varlist[0], v) + self.manage_children(inner_rule, scope) + + # @print_timing(10) + def _at_while(self, calculator, rule, scope, block): + """ + Implements @while + """ + first_condition = condition = calculator.calculate(block.argument) + while condition: + inner_rule = rule.copy() + inner_rule.unparsed_contents = block.unparsed_contents + if not self.should_scope_loop_in_rule(inner_rule): + # DEVIATION: Allow not creating a new namespace + inner_rule.namespace = rule.namespace + self.manage_children(inner_rule, scope) + condition = calculator.calculate(block.argument) + rule.options['@if'] = first_condition + + # @print_timing(10) + def _at_variables(self, calculator, rule, scope, block): + """ + Implements @variables and @vars + """ + warn_deprecated( + rule, + "@variables and @vars are deprecated. " + "Just assign variables at top-level.") + _rule = rule.copy() + _rule.unparsed_contents = block.unparsed_contents + _rule.namespace = rule.namespace + _rule.properties = [] + self.manage_children(_rule, scope) + _at_vars = _at_variables + + # @print_timing(10) + def _get_properties(self, rule, scope, block): + """ + Implements properties and variables extraction and assignment + """ + prop, raw_value = (_prop_split_re.split(block.prop, 1) + [None])[:2] + if raw_value is not None: + raw_value = raw_value.strip() + + try: + is_var = (block.prop[len(prop)] == '=') + except IndexError: + is_var = False + if is_var: + warn_deprecated(rule, "Assignment with = is deprecated; use : instead.") + calculator = self._make_calculator(rule.namespace) + prop = prop.strip() + prop = calculator.do_glob_math(prop) + if not prop: + return + + _prop = (scope or '') + prop + if is_var or prop.startswith('$') and raw_value is not None: + # Pop off any flags: !default, !global + is_default = False + is_global = True # eventually sass will default this to false + while True: + splits = raw_value.rsplit(None, 1) + if len(splits) < 2 or not splits[1].startswith('!'): + break + + raw_value, flag = splits + if flag == '!default': + is_default = True + elif flag == '!global': + is_global = True + else: + raise ValueError("Unrecognized flag: {0}".format(flag)) + + # Variable assignment + _prop = normalize_var(_prop) + try: + existing_value = rule.namespace.variable(_prop) + except KeyError: + existing_value = None + + is_defined = existing_value is not None and not existing_value.is_null + if is_default and is_defined: + pass + else: + if is_defined and prop.startswith('$') and prop[1].isupper(): + log.warn("Constant %r redefined", prop) + + # Variable assignment is an expression, so it always performs + # real division + value = calculator.calculate(raw_value, divide=True) + rule.namespace.set_variable( + _prop, value, local_only=not is_global) + else: + # Regular property destined for output + _prop = calculator.apply_vars(_prop) + if raw_value is None: + value = None + else: + value = calculator.calculate(raw_value) + + if value is None: + pass + elif isinstance(value, six.string_types): + # TODO kill this branch + pass + else: + if value.is_null: + return + style = rule.legacy_compiler_options.get( + 'style', self.compiler.output_style) + compress = style == 'compressed' + value = value.render(compress=compress) + + rule.properties.append((_prop, value)) + + # @print_timing(10) + def _nest_at_rules(self, rule, scope, block): + """ + Implements @-blocks + """ + # TODO handle @charset, probably? + # Interpolate the current block + # TODO this seems like it should be done in the block header. and more + # generally? + calculator = self._make_calculator(rule.namespace) + if block.header.argument: + # TODO is this correct? do ALL at-rules ALWAYS allow both vars and + # interpolation? + node = calculator.parse_vars_and_interpolations( + block.header.argument) + block.header.argument = node.evaluate(calculator).render() + + # TODO merge into RuleAncestry + new_ancestry = list(rule.ancestry.headers) + if block.directive == '@media' and new_ancestry: + for i, header in reversed(list(enumerate(new_ancestry))): + if header.is_selector: + continue + elif header.directive == '@media': + new_ancestry[i] = BlockAtRuleHeader( + '@media', + "%s and %s" % (header.argument, block.argument)) + break + else: + new_ancestry.insert(i, block.header) + else: + new_ancestry.insert(0, block.header) + else: + new_ancestry.append(block.header) + + rule.descendants += 1 + new_rule = SassRule( + 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, + options=rule.options.copy(), + #properties + #extends_selectors + ancestry=RuleAncestry(new_ancestry), + + namespace=rule.namespace.derive(), + nested=rule.nested + 1, + ) + self.rules.append(new_rule) + rule.namespace.use_import(rule.source_file) + self.manage_children(new_rule, scope) + + self._warn_unused_imports(new_rule) + + # @print_timing(10) + def _nest_rules(self, rule, scope, block): + """ + Implements Nested CSS rules + """ + calculator = self._make_calculator(rule.namespace) + raw_selectors = calculator.do_glob_math(block.prop) + # DEVIATION: ruby sass doesn't support bare variables in selectors + raw_selectors = calculator.apply_vars(raw_selectors) + c_selectors, c_parents = self.parse_selectors(raw_selectors) + if c_parents: + warn_deprecated( + rule, + "The XCSS 'a extends b' syntax is deprecated. " + "Use 'a { @extend b; }' instead." + ) + + new_ancestry = rule.ancestry.with_nested_selectors(c_selectors) + + rule.descendants += 1 + new_rule = SassRule( + 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, + options=rule.options.copy(), + #properties + extends_selectors=c_parents, + ancestry=new_ancestry, + + namespace=rule.namespace.derive(), + nested=rule.nested + 1, + ) + self.rules.append(new_rule) + rule.namespace.use_import(rule.source_file) + self.manage_children(new_rule, scope) + + self._warn_unused_imports(new_rule) + + # @print_timing(3) + def apply_extends(self, rules): + """Run through the given rules and translate all the pending @extends + declarations into real selectors on parent rules. + + The list is modified in-place and also sorted in dependency order. + """ + # Game plan: for each rule that has an @extend, add its selectors to + # every rule that matches that @extend. + # First, rig a way to find arbitrary selectors quickly. Most selectors + # revolve around elements, classes, and IDs, so parse those out and use + # them as a rough key. Ignore order and duplication for now. + key_to_selectors = defaultdict(set) + selector_to_rules = defaultdict(set) + rule_selector_order = {} + order = 0 + for rule in rules: + for selector in rule.selectors: + for key in selector.lookup_key(): + key_to_selectors[key].add(selector) + selector_to_rules[selector].add(rule) + rule_selector_order[rule, selector] = order + order += 1 + + # Now go through all the rules with an @extends and find their parent + # rules. + for rule in rules: + for selector in rule.extends_selectors: + # This is a little dirty. intersection isn't a class method. + # Don't think about it too much. + candidates = set.intersection(*( + key_to_selectors[key] for key in selector.lookup_key())) + extendable_selectors = [ + candidate for candidate in candidates + if candidate.is_superset_of(selector)] + + if not extendable_selectors: + # TODO implement !optional + warn_deprecated( + rule, + "Can't find any matching rules to extend {0!r} -- this " + "will be fatal in 2.0, unless !optional is specified!" + .format(selector.render())) + continue + + # Armed with a set of selectors that this rule can extend, do + # some substitution and modify the appropriate parent rules. + # One tricky bit: it's possible we're extending two selectors + # that both exist in the same parent rule, in which case we + # want to extend in the order the original selectors appear in + # that rule. + known_parents = [] + for extendable_selector in extendable_selectors: + parent_rules = selector_to_rules[extendable_selector] + for parent_rule in parent_rules: + if parent_rule is rule: + # Don't extend oneself + continue + known_parents.append( + (parent_rule, extendable_selector)) + # This will put our parents back in their original order + known_parents.sort(key=rule_selector_order.__getitem__) + + for parent_rule, extendable_selector in known_parents: + more_parent_selectors = [] + + for rule_selector in rule.selectors: + more_parent_selectors.extend( + extendable_selector.substitute( + selector, rule_selector)) + + for parent in more_parent_selectors: + # Update indices, in case later rules try to extend + # this one + for key in parent.lookup_key(): + key_to_selectors[key].add(parent) + selector_to_rules[parent].add(parent_rule) + rule_selector_order[parent_rule, parent] = order + order += 1 + + parent_rule.ancestry = ( + parent_rule.ancestry.with_more_selectors( + more_parent_selectors)) + + # Remove placeholder-only rules + return [rule for rule in rules if not rule.is_pure_placeholder] + + # @print_timing(3) + def create_css(self, rules): + """ + Generate the final CSS string + """ + style = rules[0].legacy_compiler_options.get( + 'style', self.compiler.output_style) + debug_info = self.compiler.generate_source_map + + if style == 'legacy': + sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg = True, ' ', ' ', False, '', '\n', '\n', '\n', debug_info + elif style == 'compressed': + sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg = False, '', '', False, '', '', '', '', False + elif style == 'compact': + sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg = True, ' ', '', False, '\n', ' ', '\n', ' ', debug_info + elif style == 'expanded': + sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg = True, ' ', ' ', False, '\n', '\n', '\n', '\n', debug_info + else: # if style == 'nested': + sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg = True, ' ', ' ', True, '\n', '\n', '\n', ' ', debug_info + + return self._create_css(rules, sc, sp, tb, nst, srnl, nl, rnl, lnl, dbg) + + def _textwrap(self, txt, width=70): + if not hasattr(self, '_textwrap_wordsep_re'): + self._textwrap_wordsep_re = re.compile(r'(?<=,)\s+') + self._textwrap_strings_re = re.compile(r'''(["'])(?:(?!\1)[^\\]|\\.)*\1''') + + # First, remove commas from anything within strings (marking commas as \0): + def _repl(m): + ori = m.group(0) + fin = ori.replace(',', '\0') + if ori != fin: + subs[fin] = ori + return fin + subs = {} + txt = self._textwrap_strings_re.sub(_repl, txt) + + # Mark split points for word separators using (marking spaces with \1): + txt = self._textwrap_wordsep_re.sub('\1', txt) + + # Replace all the strings back: + for fin, ori in subs.items(): + txt = txt.replace(fin, ori) + + # Split in chunks: + chunks = txt.split('\1') + + # Break in lines of at most long_width width appending chunks: + ln = '' + lines = [] + long_width = int(width * 1.2) + for chunk in chunks: + _ln = ln + ' ' if ln else '' + _ln += chunk + if len(ln) >= width or len(_ln) >= long_width: + if ln: + lines.append(ln) + _ln = chunk + ln = _ln + if ln: + lines.append(ln) + + return lines + + def _create_css(self, rules, sc=True, sp=' ', tb=' ', nst=True, srnl='\n', nl='\n', rnl='\n', lnl='', debug_info=False): + super_selector = self.compiler.super_selector + if super_selector: + super_selector += ' ' + + skip_selectors = False + + prev_ancestry_headers = [] + + total_selectors = 0 + + result = '' + dangling_property = False + separate = False + nesting = current_nesting = last_nesting = -1 if nst else 0 + nesting_stack = [] + for rule in rules: + nested = rule.nested + if nested <= 1: + separate = True + + if nst: + last_nesting = current_nesting + current_nesting = nested + + delta_nesting = current_nesting - last_nesting + if delta_nesting > 0: + nesting_stack += [nesting] * delta_nesting + elif delta_nesting < 0: + nesting_stack = nesting_stack[:delta_nesting] + nesting = nesting_stack[-1] + + if rule.is_empty: + continue + + if nst: + nesting += 1 + + ancestry = rule.ancestry + ancestry_len = len(ancestry) + + first_mismatch = 0 + for i, (old_header, new_header) in enumerate(zip(prev_ancestry_headers, ancestry.headers)): + if old_header != new_header: + first_mismatch = i + break + + # When sc is False, sets of properties are printed without a + # trailing semicolon. If the previous block isn't being closed, + # that trailing semicolon needs adding in to separate the last + # property from the next rule. + if not sc and dangling_property and first_mismatch >= len(prev_ancestry_headers): + result += ';' + + # Close blocks and outdent as necessary + for i in range(len(prev_ancestry_headers), first_mismatch, -1): + result += tb * (i - 1) + '}' + rnl + + # Open new blocks as necessary + for i in range(first_mismatch, ancestry_len): + header = ancestry.headers[i] + + if separate: + if result: + result += srnl + separate = False + if debug_info: + def _print_debug_info(filename, lineno): + if debug_info == 'comments': + result = tb * (i + nesting) + "/* file: %s, line: %s */" % (filename, lineno) + nl + else: + filename = _escape_chars_re.sub(r'\\\1', filename) + result = tb * (i + nesting) + "@media -sass-debug-info{filename{font-family:file\:\/\/%s}line{font-family:\\00003%s}}" % (filename, lineno) + nl + return result + + if rule.lineno and rule.source_file: + result += _print_debug_info(rule.source_file.path, rule.lineno) + + if rule.from_lineno and rule.from_source_file: + result += _print_debug_info(rule.from_source_file.path, rule.from_lineno) + + if header.is_selector: + header_string = header.render(sep=',' + sp, super_selector=super_selector) + if nl: + header_string = (nl + tb * (i + nesting)).join(self._textwrap(header_string)) + else: + header_string = header.render() + result += tb * (i + nesting) + header_string + sp + '{' + nl + + if header.is_selector: + total_selectors += 1 + + prev_ancestry_headers = ancestry.headers + dangling_property = False + + if not skip_selectors: + result += self._print_properties(rule.properties, sc, sp, tb * (ancestry_len + nesting), nl, lnl) + dangling_property = True + + # Close all remaining blocks + for i in reversed(range(len(prev_ancestry_headers))): + result += tb * i + '}' + rnl + + # Always end with a newline, even in compressed mode + if not result.endswith('\n'): + result += '\n' + + return (result, total_selectors) + + def _print_properties(self, properties, sc=True, sp=' ', tb='', nl='\n', lnl=' '): + result = '' + last_prop_index = len(properties) - 1 + for i, (name, value) in enumerate(properties): + if value is None: + prop = name + elif value: + if nl: + value = (nl + tb + tb).join(self._textwrap(value)) + prop = name + ':' + sp + value + else: + # Empty string means there's supposed to be a value but it + # evaluated to nothing; skip this + # TODO interacts poorly with last_prop_index + continue + + if i == last_prop_index: + if sc: + result += tb + prop + ';' + lnl + else: + result += tb + prop + lnl + else: + result += tb + prop + ';' + nl + return result + + +class SassReturn(SassBaseError): + """Special control-flow exception used to hop up the stack from a Sass + function's ``@return``. + """ + def __init__(self, retval): + super(SassReturn, self).__init__() + self.retval = retval + + def __str__(self): + return "Returning {0!r}".format(self.retval) diff --git a/aip_site/vendor/pyscss/scss/config.py b/aip_site/vendor/pyscss/scss/config.py new file mode 100644 index 0000000..1c500b9 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/config.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +################################################################################ +# Configuration: +DEBUG = False +VERBOSITY = 1 + +import os +PROJECT_ROOT = os.path.normpath(os.path.dirname(os.path.abspath(__file__))) + +# Sass @import load_paths: +LOAD_PATHS = os.path.join(PROJECT_ROOT, 'sass/frameworks') + +# Assets path, where new sprite files are created (defaults to STATIC_ROOT + '/assets'): +ASSETS_ROOT = None +# Cache files path, where cache files are saved (defaults to ASSETS_ROOT): +CACHE_ROOT = None +# Assets path, where new sprite files are created: +STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') +FONTS_ROOT = None # default: STATIC_ROOT +IMAGES_ROOT = None # default: STATIC_ROOT + +# Urls for the static and assets: +ASSETS_URL = 'static/assets/' +STATIC_URL = 'static/' +FONTS_URL = None # default: STATIC_URL +IMAGES_URL = None # default: STATIC_URL + +# Rendering style. Available values are 'nested', 'expanded', 'compact', 'compressed' and 'legacy' (defaults to 'nested'): +STYLE = 'nested' + +# Use a different scope inside control structures create a scope (defaults to create new scopes for control structures, same as Sass): +CONTROL_SCOPING = True + +# Throw fatal errors when finding undefined variables: +FATAL_UNDEFINED = True + +SPRTE_MAP_DIRECTION = 'vertical' diff --git a/aip_site/vendor/pyscss/scss/cssdefs.py b/aip_site/vendor/pyscss/scss/cssdefs.py new file mode 100644 index 0000000..b1279b0 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/cssdefs.py @@ -0,0 +1,516 @@ +"""Constants and functions defined by the CSS specification, not specific to +Sass. +""" +from fractions import Fraction +from math import pi +import re + +import six + +# ------------------------------------------------------------------------------ +# Built-in CSS color names +# See: http://www.w3.org/TR/css3-color/#svg-color + +COLOR_NAMES = { + 'aliceblue': (240, 248, 255, 1), + 'antiquewhite': (250, 235, 215, 1), + 'aqua': (0, 255, 255, 1), + 'aquamarine': (127, 255, 212, 1), + 'azure': (240, 255, 255, 1), + 'beige': (245, 245, 220, 1), + 'bisque': (255, 228, 196, 1), + 'black': (0, 0, 0, 1), + 'blanchedalmond': (255, 235, 205, 1), + 'blue': (0, 0, 255, 1), + 'blueviolet': (138, 43, 226, 1), + 'brown': (165, 42, 42, 1), + 'burlywood': (222, 184, 135, 1), + 'cadetblue': (95, 158, 160, 1), + 'chartreuse': (127, 255, 0, 1), + 'chocolate': (210, 105, 30, 1), + 'coral': (255, 127, 80, 1), + 'cornflowerblue': (100, 149, 237, 1), + 'cornsilk': (255, 248, 220, 1), + 'crimson': (220, 20, 60, 1), + 'cyan': (0, 255, 255, 1), + 'darkblue': (0, 0, 139, 1), + 'darkcyan': (0, 139, 139, 1), + 'darkgoldenrod': (184, 134, 11, 1), + 'darkgray': (169, 169, 169, 1), + 'darkgreen': (0, 100, 0, 1), + 'darkkhaki': (189, 183, 107, 1), + 'darkmagenta': (139, 0, 139, 1), + 'darkolivegreen': (85, 107, 47, 1), + 'darkorange': (255, 140, 0, 1), + 'darkorchid': (153, 50, 204, 1), + 'darkred': (139, 0, 0, 1), + 'darksalmon': (233, 150, 122, 1), + 'darkseagreen': (143, 188, 143, 1), + 'darkslateblue': (72, 61, 139, 1), + 'darkslategray': (47, 79, 79, 1), + 'darkturquoise': (0, 206, 209, 1), + 'darkviolet': (148, 0, 211, 1), + 'deeppink': (255, 20, 147, 1), + 'deepskyblue': (0, 191, 255, 1), + 'dimgray': (105, 105, 105, 1), + 'dodgerblue': (30, 144, 255, 1), + 'firebrick': (178, 34, 34, 1), + 'floralwhite': (255, 250, 240, 1), + 'forestgreen': (34, 139, 34, 1), + 'fuchsia': (255, 0, 255, 1), + 'gainsboro': (220, 220, 220, 1), + 'ghostwhite': (248, 248, 255, 1), + 'gold': (255, 215, 0, 1), + 'goldenrod': (218, 165, 32, 1), + 'gray': (128, 128, 128, 1), + 'green': (0, 128, 0, 1), + 'greenyellow': (173, 255, 47, 1), + 'honeydew': (240, 255, 240, 1), + 'hotpink': (255, 105, 180, 1), + 'indianred': (205, 92, 92, 1), + 'indigo': (75, 0, 130, 1), + 'ivory': (255, 255, 240, 1), + 'khaki': (240, 230, 140, 1), + 'lavender': (230, 230, 250, 1), + 'lavenderblush': (255, 240, 245, 1), + 'lawngreen': (124, 252, 0, 1), + 'lemonchiffon': (255, 250, 205, 1), + 'lightblue': (173, 216, 230, 1), + 'lightcoral': (240, 128, 128, 1), + 'lightcyan': (224, 255, 255, 1), + 'lightgoldenrodyellow': (250, 250, 210, 1), + 'lightgreen': (144, 238, 144, 1), + 'lightgrey': (211, 211, 211, 1), + 'lightpink': (255, 182, 193, 1), + 'lightsalmon': (255, 160, 122, 1), + 'lightseagreen': (32, 178, 170, 1), + 'lightskyblue': (135, 206, 250, 1), + 'lightslategray': (119, 136, 153, 1), + 'lightsteelblue': (176, 196, 222, 1), + 'lightyellow': (255, 255, 224, 1), + 'lime': (0, 255, 0, 1), + 'limegreen': (50, 205, 50, 1), + 'linen': (250, 240, 230, 1), + 'magenta': (255, 0, 255, 1), + 'maroon': (128, 0, 0, 1), + 'mediumaquamarine': (102, 205, 170, 1), + 'mediumblue': (0, 0, 205, 1), + 'mediumorchid': (186, 85, 211, 1), + 'mediumpurple': (147, 112, 219, 1), + 'mediumseagreen': (60, 179, 113, 1), + 'mediumslateblue': (123, 104, 238, 1), + 'mediumspringgreen': (0, 250, 154, 1), + 'mediumturquoise': (72, 209, 204, 1), + 'mediumvioletred': (199, 21, 133, 1), + 'midnightblue': (25, 25, 112, 1), + 'mintcream': (245, 255, 250, 1), + 'mistyrose': (255, 228, 225, 1), + 'moccasin': (255, 228, 181, 1), + 'navajowhite': (255, 222, 173, 1), + 'navy': (0, 0, 128, 1), + 'oldlace': (253, 245, 230, 1), + 'olive': (128, 128, 0, 1), + 'olivedrab': (107, 142, 35, 1), + 'orange': (255, 165, 0, 1), + 'orangered': (255, 69, 0, 1), + 'orchid': (218, 112, 214, 1), + 'palegoldenrod': (238, 232, 170, 1), + 'palegreen': (152, 251, 152, 1), + 'paleturquoise': (175, 238, 238, 1), + 'palevioletred': (219, 112, 147, 1), + 'papayawhip': (255, 239, 213, 1), + 'peachpuff': (255, 218, 185, 1), + 'peru': (205, 133, 63, 1), + 'pink': (255, 192, 203, 1), + 'plum': (221, 160, 221, 1), + 'powderblue': (176, 224, 230, 1), + 'purple': (128, 0, 128, 1), + 'rebeccapurple': (0x66, 0x33, 0x99, 1), + 'red': (255, 0, 0, 1), + 'rosybrown': (188, 143, 143, 1), + 'royalblue': (65, 105, 225, 1), + 'saddlebrown': (139, 69, 19, 1), + 'salmon': (250, 128, 114, 1), + 'sandybrown': (244, 164, 96, 1), + 'seagreen': (46, 139, 87, 1), + 'seashell': (255, 245, 238, 1), + 'sienna': (160, 82, 45, 1), + 'silver': (192, 192, 192, 1), + 'skyblue': (135, 206, 235, 1), + 'slateblue': (106, 90, 205, 1), + 'slategray': (112, 128, 144, 1), + 'snow': (255, 250, 250, 1), + 'springgreen': (0, 255, 127, 1), + 'steelblue': (70, 130, 180, 1), + 'tan': (210, 180, 140, 1), + 'teal': (0, 128, 128, 1), + 'thistle': (216, 191, 216, 1), + 'tomato': (255, 99, 71, 1), + 'transparent': (0, 0, 0, 0), + 'turquoise': (64, 224, 208, 1), + 'violet': (238, 130, 238, 1), + 'wheat': (245, 222, 179, 1), + 'white': (255, 255, 255, 1), + 'whitesmoke': (245, 245, 245, 1), + 'yellow': (255, 255, 0, 1), + 'yellowgreen': (154, 205, 50, 1), +} +COLOR_LOOKUP = dict((v, k) for (k, v) in COLOR_NAMES.items()) + + +# ------------------------------------------------------------------------------ +# Built-in CSS units +# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#numeric-types + +# Maps units to a set of common units per type, with conversion factors +BASE_UNIT_CONVERSIONS = { + # Lengths + 'mm': (1, 'mm'), + 'cm': (10, 'mm'), + 'in': (Fraction(254, 10), 'mm'), + 'px': (Fraction(254, 960), 'mm'), + 'pt': (Fraction(254, 720), 'mm'), + 'pc': (Fraction(254, 60), 'mm'), + + # Angles + 'deg': (Fraction(1, 360), 'turn'), + 'grad': (Fraction(1, 400), 'turn'), + 'rad': (Fraction.from_float(pi / 2), 'turn'), + 'turn': (1, 'turn'), + + # Times + 'ms': (1, 'ms'), + 's': (1000, 'ms'), + + # Frequencies + 'hz': (1, 'hz'), + 'khz': (1000, 'hz'), + + # Resolutions + 'dpi': (1, 'dpi'), + 'dpcm': (Fraction(254 / 100), 'dpi'), + 'dppx': (96, 'dpi'), +} + + +def get_conversion_factor(unit): + """Look up the "base" unit for this unit and the factor for converting to + it. + + Returns a 2-tuple of `factor, base_unit`. + """ + if unit in BASE_UNIT_CONVERSIONS: + return BASE_UNIT_CONVERSIONS[unit] + else: + return 1, unit + + +def convert_units_to_base_units(units): + """Convert a set of units into a set of "base" units. + + Returns a 2-tuple of `factor, new_units`. + """ + total_factor = 1 + new_units = [] + for unit in units: + if unit not in BASE_UNIT_CONVERSIONS: + continue + + factor, new_unit = BASE_UNIT_CONVERSIONS[unit] + total_factor *= factor + new_units.append(new_unit) + + new_units.sort() + return total_factor, tuple(new_units) + + +def count_base_units(units): + """Returns a dict mapping names of base units to how many times they + appear in the given iterable of units. Effectively this counts how + many length units you have, how many time units, and so forth. + """ + ret = {} + for unit in units: + factor, base_unit = get_conversion_factor(unit) + + ret.setdefault(base_unit, 0) + ret[base_unit] += 1 + + return ret + + +def cancel_base_units(units, to_remove): + """Given a list of units, remove a specified number of each base unit. + + Arguments: + units: an iterable of units + to_remove: a mapping of base_unit => count, such as that returned from + count_base_units + + Returns a 2-tuple of (factor, remaining_units). + """ + + # Copy the dict since we're about to mutate it + to_remove = to_remove.copy() + remaining_units = [] + total_factor = Fraction(1) + + for unit in units: + factor, base_unit = get_conversion_factor(unit) + if not to_remove.get(base_unit, 0): + remaining_units.append(unit) + continue + + total_factor *= factor + to_remove[base_unit] -= 1 + + return total_factor, remaining_units + + +# A fixed set of units can be omitted when the value is 0 +# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#lengths +ZEROABLE_UNITS = frozenset(( + # Relative lengths + 'em', 'ex', 'ch', 'rem', + # Viewport + 'vw', 'vh', 'vmin', 'vmax', + # Absolute lengths + 'cm', 'mm', 'in', 'px', 'pt', 'pc', +)) + + +# ------------------------------------------------------------------------------ +# Built-in CSS function reference + +# Known function names +BUILTIN_FUNCTIONS = frozenset([ + # CSS2 + 'attr', 'counter', 'counters', 'url', 'rgb', 'rect', + + # CSS3 values: http://www.w3.org/TR/css3-values/ + 'calc', 'min', 'max', 'cycle', + + # CSS3 colors: http://www.w3.org/TR/css3-color/ + 'rgba', 'hsl', 'hsla', + + # CSS3 fonts: http://www.w3.org/TR/css3-fonts/ + 'local', 'format', + + # CSS3 images: http://www.w3.org/TR/css3-images/ + 'image', 'element', + 'linear-gradient', 'radial-gradient', + 'repeating-linear-gradient', 'repeating-radial-gradient', + + # CSS3 transforms: http://www.w3.org/TR/css3-transforms/ + 'perspective', + 'matrix', 'matrix3d', + 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d', + 'translate', 'translateX', 'translateY', 'translateZ', 'translate3d', + 'scale', 'scaleX', 'scaleY', 'scaleZ', 'scale3d', + 'skew', 'skewX', 'skewY', + + # CSS3 transitions: http://www.w3.org/TR/css3-transitions/ + 'cubic-bezier', 'steps', + + # CSS filter effects: + # https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html + 'grayscale', 'sepia', 'saturate', 'hue-rotate', 'invert', 'opacity', + 'brightness', 'contrast', 'blur', 'drop-shadow', 'custom', + + # CSS shapes + # https://www.w3.org/TR/css-shapes-1/ + 'inset', 'circle', 'ellipse', 'polygon', + + # CSS4 image module: + # http://dev.w3.org/csswg/css-images/ + 'image-set', 'cross-fade', + 'conic-gradient', 'repeating-conic-gradient', + + # Others + 'color-stop', # Older version of CSS3 gradients + 'mask', # ??? + 'from', 'to', # Very old WebKit gradient syntax +]) + + +def is_builtin_css_function(name): + """Returns whether the given `name` looks like the name of a builtin CSS + function. + + Unrecognized functions not in this list produce warnings. + """ + name = name.replace('_', '-') + + if name in BUILTIN_FUNCTIONS: + return True + + # Vendor-specific functions (-foo-bar) are always okay + if name[0] == '-' and '-' in name[1:]: + return True + + return False + + +# ------------------------------------------------------------------------------ +# CSS character set determination +# Based upon: http://www.w3.org/TR/CSS2/syndata.html#charset + +def determine_encoding(buf): + """Return the appropriate encoding for the given CSS source, according to + the CSS charset rules. + + `buf` may be either a string or bytes. + """ + # The ultimate default is utf8; bravo, W3C + bom_encoding = 'UTF-8' + + if not buf: + # What + return bom_encoding + + if isinstance(buf, six.text_type): + # We got a file that, for whatever reason, produces already-decoded + # text. Check for the BOM (which is useless now) and believe + # whatever's in the @charset. + if buf[0] == '\ufeff': + buf = buf[0:] + + # This is pretty similar to the code below, but without any encoding + # double-checking. + charset_start = '@charset "' + charset_end = '";' + if buf.startswith(charset_start): + start = len(charset_start) + end = buf.index(charset_end, start) + return buf[start:end] + else: + return bom_encoding + + # BOMs + if buf[:3] == b'\xef\xbb\xbf': + bom_encoding = 'UTF-8' + buf = buf[3:] + if buf[:4] == b'\x00\x00\xfe\xff': + bom_encoding = 'UTF-32BE' + buf = buf[4:] + elif buf[:4] == b'\xff\xfe\x00\x00': + bom_encoding = 'UTF-32LE' + buf = buf[4:] + if buf[:4] == b'\x00\x00\xff\xfe': + raise UnicodeError("UTF-32-2143 is not supported") + elif buf[:4] == b'\xfe\xff\x00\x00': + raise UnicodeError("UTF-32-2143 is not supported") + elif buf[:2] == b'\xfe\xff': + bom_encoding = 'UTF-16BE' + buf = buf[2:] + elif buf[:2] == b'\xff\xfe': + bom_encoding = 'UTF-16LE' + buf = buf[2:] + + # The spec requires exactly this syntax; no escapes or extra spaces or + # other shenanigans, thank goodness. + charset_start = '@charset "'.encode(bom_encoding) + charset_end = '";'.encode(bom_encoding) + if buf.startswith(charset_start): + start = len(charset_start) + end = buf.index(charset_end, start) + encoded_encoding = buf[start:end] + encoding = encoded_encoding.decode(bom_encoding) + + # Ensure that decoding with the specified encoding actually produces + # the same @charset rule + encoded_charset = buf[:end + len(charset_end)] + if (encoded_charset.decode(encoding) != + encoded_charset.decode(bom_encoding)): + raise UnicodeError( + "@charset {0} is incompatible with detected encoding {1}" + .format(bom_encoding, encoding)) + else: + # With no @charset, believe the BOM + encoding = bom_encoding + + return encoding + + +# ------------------------------------------------------------------------------ +# Bits and pieces of the official CSS grammar + +# These are the only pseudo-elements allowed to be specified with a single +# colon, for backwards compatibility +CSS2_PSEUDO_ELEMENTS = frozenset(( + ':after', + ':before', + ':first-line', + ':first-letter', +)) + +# CSS escape sequences are either a backslash followed by a single character, +# or a backslash followed by one to six hex digits and a single optional +# whitespace. Escaped newlines become nothing. +# Ref: http://dev.w3.org/csswg/css-syntax-3/#consume-an-escaped-code-point +escape_rx = re.compile(r"(?s)\\([0-9a-fA-F]{1,6})[\n\t ]?|\\(.)|\\\n") + + +def _unescape_one(match): + if match.group(1) is not None: + try: + return six.unichr(int(match.group(1), 16)) + except ValueError: + return (r'\U%08x' % int(match.group(1), 16)).decode( + 'unicode-escape') + elif match.group(2) is not None: + return match.group(2) + else: + return six.text_type() + + +def unescape(string): + """Given a raw CSS string (i.e. taken directly from CSS source with no + processing), eliminate all backslash escapes. + """ + return escape_rx.sub(_unescape_one, string) + + +# ------------------------------------------------------------------------------ +# Ad-hoc regexes specific to pyscss + +_expr_glob_re = re.compile(r''' + \#\{(.*?)\} # Global Interpolation only +''', re.VERBOSE) + +# XXX these still need to be fixed; the //-in-functions thing is a chumpy hack +_ml_comment_re = re.compile(r'\/\*(.*?)\*\/', re.DOTALL) +_sl_comment_re = re.compile(r'(?".format(type(self).__name__) + + def handle_import(self, name, compilation, rule): + """Attempt to resolve an import. Called once for every Sass string + listed in an ``@import`` statement. Imports that Sass dictates should + be converted to plain CSS imports do NOT trigger this hook. + + So this:: + + @import url(foo), "bar", "baz"; + + would call `handle_import` twice: once with "bar", once with "baz". + + Return a :class:`scss.source.SourceFile` if you want to handle the + import, or None if you don't. (This method returns None by default, so + if you don't care about hooking imports, just don't implement it.) + This method is tried on every registered `Extension` in order, until + one of them returns successfully. + + A good example is the core Sass import machinery, which is implemented + with this hook; see the source code of the core extension. + """ + pass + + +class NamespaceAdapterExtension(Extension): + """Trivial wrapper that adapts a bare :class:`scss.namespace.Namespace` + into a full extension. + """ + + def __init__(self, namespace): + self.namespace = namespace diff --git a/aip_site/vendor/pyscss/scss/extension/bootstrap.py b/aip_site/vendor/pyscss/scss/extension/bootstrap.py new file mode 100644 index 0000000..bd1b75e --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/bootstrap.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +from scss.extension import Extension +from scss.extension.compass.helpers import _font_url +from scss.extension.compass.images import _image_url +from scss.namespace import Namespace + + +class BootstrapExtension(Extension): + name = 'bootstrap' + namespace = Namespace() + +ns = BootstrapExtension.namespace + + +@ns.declare +def twbs_font_path(path): + return _font_url(path, False, True, False) + + +@ns.declare +def twbs_image_path(path): + return _image_url(path, False, True, None, None, False, None, None, None, None) diff --git a/aip_site/vendor/pyscss/scss/extension/compass/__init__.py b/aip_site/vendor/pyscss/scss/extension/compass/__init__.py new file mode 100644 index 0000000..02ca732 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/__init__.py @@ -0,0 +1,114 @@ +"""Extension providing Compass support.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import glob +import os.path + +import scss.config as config +from scss.extension import Extension +from scss.namespace import Namespace +from scss.source import SourceFile +from scss.types import Boolean +from scss.types import Number +from scss.types import String + + +class CompassExtension(Extension): + name = 'compass' + namespace = Namespace() + + def handle_import(self, name, compilation, rule): + """Implementation of Compass's "magic" imports, which generate + spritesheets on the fly, given either a wildcard or the name of a + directory. + """ + from .sprites import sprite_map + + # TODO check that the found file is actually under the root + if callable(config.STATIC_ROOT): + files = sorted(config.STATIC_ROOT(name)) + else: + glob_path = os.path.join(config.STATIC_ROOT, name) + files = glob.glob(glob_path) + files = sorted((fn[len(config.STATIC_ROOT):], None) for fn in files) + + if not files: + return + + # Build magic context + calculator = compilation._make_calculator(rule.namespace) + map_name = os.path.normpath(os.path.dirname(name)).replace('\\', '_').replace('/', '_') + kwargs = {} + + # TODO this is all kinds of busted. rule.context hasn't existed for + # ages. + def setdefault(var, val): + _var = '$' + map_name + '-' + var + if _var in rule.context: + kwargs[var] = calculator.interpolate(rule.context[_var], rule, self._library) + else: + rule.context[_var] = val + kwargs[var] = calculator.interpolate(val, rule, self._library) + return rule.context[_var] + + setdefault('sprite-base-class', String('.' + map_name + '-sprite', quotes=None)) + setdefault('sprite-dimensions', Boolean(False)) + position = setdefault('position', Number(0, '%')) + spacing = setdefault('spacing', Number(0)) + repeat = setdefault('repeat', String('no-repeat', quotes=None)) + names = tuple(os.path.splitext(os.path.basename(file))[0] for file, storage in files) + for n in names: + setdefault(n + '-position', position) + setdefault(n + '-spacing', spacing) + setdefault(n + '-repeat', repeat) + rule.context['$' + map_name + '-' + 'sprites'] = sprite_map(name, **kwargs) + generated_code = ''' + @import "compass/utilities/sprites/base"; + + // All sprites should extend this class + // The %(map_name)s-sprite mixin will do so for you. + #{$%(map_name)s-sprite-base-class} { + background: $%(map_name)s-sprites; + } + + // Use this to set the dimensions of an element + // based on the size of the original image. + @mixin %(map_name)s-sprite-dimensions($name) { + @include sprite-dimensions($%(map_name)s-sprites, $name); + } + + // Move the background position to display the sprite. + @mixin %(map_name)s-sprite-position($name, $offset-x: 0, $offset-y: 0) { + @include sprite-position($%(map_name)s-sprites, $name, $offset-x, $offset-y); + } + + // Extends the sprite base class and set the background position for the desired sprite. + // It will also apply the image dimensions if $dimensions is true. + @mixin %(map_name)s-sprite($name, $dimensions: $%(map_name)s-sprite-dimensions, $offset-x: 0, $offset-y: 0) { + @extend #{$%(map_name)s-sprite-base-class}; + @include sprite($%(map_name)s-sprites, $name, $dimensions, $offset-x, $offset-y); + } + + @mixin %(map_name)s-sprites($sprite-names, $dimensions: $%(map_name)s-sprite-dimensions) { + @include sprites($%(map_name)s-sprites, $sprite-names, $%(map_name)s-sprite-base-class, $dimensions); + } + + // Generates a class for each sprited image. + @mixin all-%(map_name)s-sprites($dimensions: $%(map_name)s-sprite-dimensions) { + @include %(map_name)s-sprites(%(sprites)s, $dimensions); + } + ''' % {'map_name': map_name, 'sprites': ' '.join(names)} + + return SourceFile.from_string(generated_code) + + +__all__ = ['CompassExtension'] + +# Import child modules LAST, so they can in turn import CompassExtension from +# us +import scss.extension.compass.gradients +import scss.extension.compass.helpers +import scss.extension.compass.images +import scss.extension.compass.sprites # NOQA diff --git a/aip_site/vendor/pyscss/scss/extension/compass/gradients.py b/aip_site/vendor/pyscss/scss/extension/compass/gradients.py new file mode 100644 index 0000000..15e1200 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/gradients.py @@ -0,0 +1,435 @@ +"""Utilities for working with gradients. Inspired by Compass, but not quite +the same. +""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import base64 +import logging + +import six + +from . import CompassExtension +from .helpers import opposite_position, position +from scss.types import Color, List, Number, String +from scss.util import escape, split_params, to_float, to_str + +log = logging.getLogger(__name__) +ns = CompassExtension.namespace + + +def _is_color(value): + # currentColor is not a Sass color value, but /is/ a CSS color value + return isinstance(value, Color) or value == String('currentColor') + + +def __color_stops(percentages, *args): + if len(args) == 1: + if isinstance(args[0], (list, tuple, List)): + return list(args[0]) + elif isinstance(args[0], (String, six.string_types)): + color_stops = [] + colors = split_params(getattr(args[0], 'value', args[0])) + for color in colors: + color = color.strip() + if color.startswith('color-stop('): + s, c = split_params(color[11:].rstrip(')')) + s = s.strip() + c = c.strip() + else: + c, s = color.split() + color_stops.append((to_float(s), c)) + return color_stops + + colors = [] + stops = [] + prev_color = False + for c in args: + for c in List.from_maybe(c): + if _is_color(c): + if prev_color: + stops.append(None) + colors.append(c) + prev_color = True + elif isinstance(c, Number): + stops.append(c) + prev_color = False + + if prev_color: + stops.append(None) + stops = stops[:len(colors)] + if stops[0] is None or stops[0] == Number(0): + stops[0] = Number(0, '%') + if stops[-1] is None: + stops[-1] = Number(100, '%') + + maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')] + if maxable_stops: + max_stops = max(maxable_stops) + else: + max_stops = None + + stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops] + + init = 0 + start = None + for i, s in enumerate(stops + [1.0]): + if s is None: + if start is None: + start = i + end = i + else: + final = s + if start is not None: + stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0)) + for j in range(start, end + 1): + stops[j] = init + stride * Number(j - start + 1) + init = final + start = None + + if not max_stops or percentages: + pass + else: + stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops] + + return List(List(pair) for pair in zip(stops, colors)) + + +def _render_standard_color_stops(color_stops): + pairs = [] + for i, (stop, color) in enumerate(color_stops): + if ((i == 0 and stop == Number(0, '%')) or + (i == len(color_stops) - 1 and stop == Number(100, '%'))): + pairs.append(color) + else: + pairs.append(List([color, stop], use_comma=False)) + + return List(pairs, use_comma=True) + + +@ns.declare +def grad_color_stops(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(True, *args) + ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +def __grad_end_position(radial, color_stops): + return __grad_position(-1, 100, radial, color_stops) + + +@ns.declare +def grad_point(*p): + pos = set() + hrz = vrt = Number(0.5, '%') + for _p in p: + pos.update(String.unquoted(_p).value.split()) + if 'left' in pos: + hrz = Number(0, '%') + elif 'right' in pos: + hrz = Number(1, '%') + if 'top' in pos: + vrt = Number(0, '%') + elif 'bottom' in pos: + vrt = Number(1, '%') + return List([v for v in (hrz, vrt) if v is not None]) + + +def __grad_position(index, default, radial, color_stops): + try: + stops = Number(color_stops[index][0]) + if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1): + log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops) + except IndexError: + stops = Number(default) + return stops + + +@ns.declare +def grad_end_position(*color_stops): + color_stops = __color_stops(False, *color_stops) + return Number(__grad_end_position(False, color_stops)) + + +@ns.declare +def color_stops(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(False, *args) + ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +@ns.declare +def color_stops_in_percentages(*args): + args = List.from_maybe_starargs(args) + color_stops = __color_stops(True, *args) + ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops]) + return String.unquoted(ret) + + +def _get_gradient_position_and_angle(args): + for arg in args: + ret = None + skip = False + for a in arg: + if _is_color(a): + skip = True + break + elif isinstance(a, Number): + ret = arg + if skip: + continue + if ret is not None: + return ret + for seek in ( + 'center', + 'top', 'bottom', + 'left', 'right', + ): + if String(seek) in arg: + return arg + return None + + +def _get_gradient_shape_and_size(args): + for arg in args: + for seek in ( + 'circle', 'ellipse', + 'closest-side', 'closest-corner', + 'farthest-side', 'farthest-corner', + 'contain', 'cover', + ): + if String(seek) in arg: + return arg + return None + + +def _get_gradient_color_stops(args): + color_stops = [] + for arg in args: + for a in List.from_maybe(arg): + if _is_color(a): + color_stops.append(arg) + break + return color_stops or None + + +# TODO these functions need to be +# 1. well-defined +# 2. guaranteed to never wreck css3 syntax +# 3. updated to whatever current compass does +# 4. fixed to use a custom type instead of monkeypatching + + +@ns.declare +def radial_gradient(*args): + args = List.from_maybe_starargs(args) + + try: + # Do a rough check for standard syntax first -- `shape at position` + at_position = list(args[0]).index(String('at')) + except (IndexError, ValueError): + shape_and_size = _get_gradient_shape_and_size(args) + position_and_angle = _get_gradient_position_and_angle(args) + else: + shape_and_size = List.maybe_new(args[0][:at_position]) + position_and_angle = List.maybe_new(args[0][at_position + 1:]) + + color_stops = _get_gradient_color_stops(args) + if color_stops is None: + raise Exception('No color stops provided to radial-gradient function') + color_stops = __color_stops(False, *color_stops) + + if position_and_angle: + rendered_position = position(position_and_angle) + else: + rendered_position = None + rendered_color_stops = _render_standard_color_stops(color_stops) + + args = [] + if shape_and_size and rendered_position: + args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False)) + elif rendered_position: + args.append(rendered_position) + elif shape_and_size: + args.append(shape_and_size) + args.extend(rendered_color_stops) + + legacy_args = [] + if rendered_position: + legacy_args.append(rendered_position) + if shape_and_size: + legacy_args.append(shape_and_size) + legacy_args.extend(rendered_color_stops) + + ret = String.unquoted( + 'radial-gradient(' + ', '.join(a.render() for a in args) + ')') + + legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')' + + def to__css2(): + return String.unquoted('') + ret.to__css2 = to__css2 + + def to__moz(): + return String.unquoted('-moz-' + legacy_ret) + ret.to__moz = to__moz + + def to__pie(): + log.warn("PIE does not support radial-gradient.") + return String.unquoted('-pie-radial-gradient(unsupported)') + ret.to__pie = to__pie + + def to__webkit(): + return String.unquoted('-webkit-' + legacy_ret) + ret.to__webkit = to__webkit + + def to__owg(): + args = [ + 'radial', + grad_point(*position_and_angle) if position_and_angle is not None else 'center', + '0', + grad_point(*position_and_angle) if position_and_angle is not None else 'center', + __grad_end_position(True, color_stops), + ] + args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) + ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' + return String.unquoted(ret) + ret.to__owg = to__owg + + def to__svg(): + return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')]))) + ret.to__svg = to__svg + + return ret + + +@ns.declare +def linear_gradient(*args): + args = List.from_maybe_starargs(args) + + position_and_angle = _get_gradient_position_and_angle(args) + color_stops = _get_gradient_color_stops(args) + if color_stops is None: + raise Exception('No color stops provided to linear-gradient function') + color_stops = __color_stops(False, *color_stops) + + args = [ + position(position_and_angle) if position_and_angle is not None else None, + ] + args.extend(_render_standard_color_stops(color_stops)) + + to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')' + ret = String.unquoted(to__s) + + def to__css2(): + return String.unquoted('') + ret.to__css2 = to__css2 + + def to__moz(): + return String.unquoted('-moz-' + to__s) + ret.to__moz = to__moz + + def to__pie(): + return String.unquoted('-pie-' + to__s) + ret.to__pie = to__pie + + def to__ms(): + return String.unquoted('-ms-' + to__s) + ret.to__ms = to__ms + + def to__o(): + return String.unquoted('-o-' + to__s) + ret.to__o = to__o + + def to__webkit(): + return String.unquoted('-webkit-' + to__s) + ret.to__webkit = to__webkit + + def to__owg(): + args = [ + 'linear', + position(position_and_angle or None), + opposite_position(position_and_angle or None), + ] + args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops) + ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')' + return String.unquoted(ret) + ret.to__owg = to__owg + + def to__svg(): + return linear_svg_gradient(color_stops, position_and_angle or 'top') + ret.to__svg = to__svg + + return ret + + +@ns.declare +def radial_svg_gradient(*args): + args = List.from_maybe_starargs(args) + color_stops = args + center = None + if isinstance(args[-1], (String, Number)): + center = args[-1] + color_stops = args[:-1] + color_stops = __color_stops(False, *color_stops) + cx, cy = grad_point(center) + r = __grad_end_position(True, color_stops) + svg = __radial_svg(color_stops, cx, cy, r) + url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +@ns.declare +def linear_svg_gradient(*args): + args = List.from_maybe_starargs(args) + color_stops = args + start = None + if isinstance(args[-1], (String, Number)): + start = args[-1] + color_stops = args[:-1] + color_stops = __color_stops(False, *color_stops) + x1, y1 = grad_point(start) + x2, y2 = grad_point(opposite_position(start)) + svg = _linear_svg(color_stops, x1, y1, x2, y2) + url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +def __color_stops_svg(color_stops): + ret = ''.join('' % (to_str(s), c) for s, c in color_stops) + return ret + + +def __svg_template(gradient): + ret = '\ +\ +%s\ +\ +' % gradient + return ret + + +def _linear_svg(color_stops, x1, y1, x2, y2): + gradient = '%s' % ( + to_str(Number(x1)), + to_str(Number(y1)), + to_str(Number(x2)), + to_str(Number(y2)), + __color_stops_svg(color_stops) + ) + return __svg_template(gradient) + + +def __radial_svg(color_stops, cx, cy, r): + gradient = '%s' % ( + to_str(Number(cx)), + to_str(Number(cy)), + to_str(Number(r)), + __color_stops_svg(color_stops) + ) + return __svg_template(gradient) diff --git a/aip_site/vendor/pyscss/scss/extension/compass/helpers.py b/aip_site/vendor/pyscss/scss/extension/compass/helpers.py new file mode 100644 index 0000000..50c8d5e --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/helpers.py @@ -0,0 +1,652 @@ +"""Miscellaneous helper functions ported from Compass. + +See: http://compass-style.org/reference/compass/helpers/ + +This collection is not necessarily complete or up-to-date. +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +import logging +import math +import os.path + +import six + +from . import CompassExtension +from scss import config +from scss.types import Boolean, Function, List, Null, Number, String, Url +from scss.util import to_str, getmtime, make_data_url +import re + +log = logging.getLogger(__name__) +ns = CompassExtension.namespace + +FONT_TYPES = { + 'woff': 'woff', + 'otf': 'opentype', + 'opentype': 'opentype', + 'ttf': 'truetype', + 'truetype': 'truetype', + 'svg': 'svg', + 'eot': 'embedded-opentype' +} + + +def add_cache_buster(url, mtime): + fragment = url.split('#') + query = fragment[0].split('?') + if len(query) > 1 and query[1] != '': + cb = '&_=%s' % (mtime) + url = '?'.join(query) + cb + else: + cb = '?_=%s' % (mtime) + url = query[0] + cb + if len(fragment) > 1: + url += '#' + fragment[1] + return url + + +# ------------------------------------------------------------------------------ +# Data manipulation + +@ns.declare +def blank(*objs): + """Returns true when the object is false, an empty string, or an empty list""" + for o in objs: + if isinstance(o, Boolean): + is_blank = not o + elif isinstance(o, String): + is_blank = not len(o.value.strip()) + elif isinstance(o, List): + is_blank = all(blank(el) for el in o) + else: + is_blank = False + + if not is_blank: + return Boolean(False) + + return Boolean(True) + + +@ns.declare +def compact(*args): + """Returns a new list after removing any non-true values""" + use_comma = True + if len(args) == 1 and isinstance(args[0], List): + use_comma = args[0].use_comma + args = args[0] + + return List( + [arg for arg in args if arg], + use_comma=use_comma, + ) + + +@ns.declare +def reject(lst, *values): + """Removes the given values from the list""" + lst = List.from_maybe(lst) + values = frozenset(List.from_maybe_starargs(values)) + + ret = [] + for item in lst: + if item not in values: + ret.append(item) + return List(ret, use_comma=lst.use_comma) + + +@ns.declare +def first_value_of(*args): + if len(args) == 1 and isinstance(args[0], String): + first = args[0].value.split()[0] + return type(args[0])(first) + + args = List.from_maybe_starargs(args) + if len(args): + return args[0] + else: + return Null() + + +@ns.declare_alias('-compass-list') +def dash_compass_list(*args): + return List.from_maybe_starargs(args) + + +@ns.declare_alias('-compass-space-list') +def dash_compass_space_list(*lst): + """ + If the argument is a list, it will return a new list that is space delimited + Otherwise it returns a new, single element, space-delimited list. + """ + ret = dash_compass_list(*lst) + ret.value.pop('_', None) + return ret + + +@ns.declare_alias('-compass-slice') +def dash_compass_slice(lst, start_index, end_index=None): + start_index = Number(start_index).value + end_index = Number(end_index).value if end_index is not None else None + ret = {} + lst = List(lst) + if end_index: + # This function has an inclusive end, but Python slicing is exclusive + end_index += 1 + ret = lst.value[start_index:end_index] + return List(ret, use_comma=lst.use_comma) + + +# ------------------------------------------------------------------------------ +# Property prefixing + +@ns.declare +def prefixed(prefix, *args): + to_fnct_str = 'to_' + to_str(prefix).replace('-', '_') + for arg in List.from_maybe_starargs(args): + if hasattr(arg, to_fnct_str): + return Boolean(True) + return Boolean(False) + + +@ns.declare +def prefix(prefix, *args): + to_fnct_str = 'to_' + to_str(prefix).replace('-', '_') + args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, List): + _value = [] + for iarg in arg: + to_fnct = getattr(iarg, to_fnct_str, None) + if to_fnct: + _value.append(to_fnct()) + else: + _value.append(iarg) + args[i] = List(_value) + else: + to_fnct = getattr(arg, to_fnct_str, None) + if to_fnct: + args[i] = to_fnct() + + return List.maybe_new(args, use_comma=True) + + +@ns.declare_alias('-moz') +def dash_moz(*args): + return prefix('_moz', *args) + + +@ns.declare_alias('-svg') +def dash_svg(*args): + return prefix('_svg', *args) + + +@ns.declare_alias('-css2') +def dash_css2(*args): + return prefix('_css2', *args) + + +@ns.declare_alias('-pie') +def dash_pie(*args): + return prefix('_pie', *args) + + +@ns.declare_alias('-webkit') +def dash_webkit(*args): + return prefix('_webkit', *args) + + +@ns.declare_alias('-owg') +def dash_owg(*args): + return prefix('_owg', *args) + + +@ns.declare_alias('-khtml') +def dash_khtml(*args): + return prefix('_khtml', *args) + + +@ns.declare_alias('-ms') +def dash_ms(*args): + return prefix('_ms', *args) + + +@ns.declare_alias('-o') +def dash_o(*args): + return prefix('_o', *args) + + +# ------------------------------------------------------------------------------ +# Selector generation + +# selector-append is a Sass function +@ns.declare_alias('selector-append') +@ns.declare +def append_selector(selector, to_append): + if isinstance(selector, List): + lst = selector.value + else: + lst = String.unquoted(selector).value.split(',') + to_append = String.unquoted(to_append).value.strip() + ret = sorted(set(s.strip() + to_append for s in lst if s.strip())) + ret = dict(enumerate(ret)) + ret['_'] = ',' + return ret + + +_elements_of_type_block = 'address, article, aside, blockquote, center, dd, details, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frameset, h1, h2, h3, h4, h5, h6, header, hgroup, hr, isindex, menu, nav, noframes, noscript, ol, p, pre, section, summary, ul' +_elements_of_type_inline = 'a, abbr, acronym, audio, b, basefont, bdo, big, br, canvas, cite, code, command, datalist, dfn, em, embed, font, i, img, input, kbd, keygen, label, mark, meter, output, progress, q, rp, rt, ruby, s, samp, select, small, span, strike, strong, sub, sup, textarea, time, tt, u, var, video, wbr' +_elements_of_type_table = 'table' +_elements_of_type_list_item = 'li' +_elements_of_type_table_row_group = 'tbody' +_elements_of_type_table_header_group = 'thead' +_elements_of_type_table_footer_group = 'tfoot' +_elements_of_type_table_row = 'tr' +_elements_of_type_table_cel = 'td, th' +_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary' +_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr' +_elements_of_type_html5 = 'article, aside, audio, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, menu, meter, nav, output, progress, rp, rt, ruby, section, summary, time, video, wbr' +_elements_of_type = { + 'block': sorted(_elements_of_type_block.replace(' ', '').split(',')), + 'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')), + 'table': sorted(_elements_of_type_table.replace(' ', '').split(',')), + 'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')), + 'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')), + 'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')), + 'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')), + 'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')), + 'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')), + 'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')), +} + + +@ns.declare +def elements_of_type(display): + d = String.unquoted(display) + ret = _elements_of_type.get(d.value, None) + if ret is None: + raise Exception("Elements of type '%s' not found!" % d.value) + return List(map(String, ret), use_comma=True) + + +@ns.declare +def enumerate_(prefix, frm, through, separator='-'): + separator = String.unquoted(separator).value + try: + frm = int(getattr(frm, 'value', frm)) + except ValueError: + frm = 1 + try: + through = int(getattr(through, 'value', through)) + except ValueError: + through = frm + if frm > through: + # DEVIATION: allow reversed enumerations (and ranges as range() uses enumerate, like '@for .. from .. through') + frm, through = through, frm + rev = reversed + else: + rev = lambda x: x + + ret = [] + for i in rev(range(frm, through + 1)): + if prefix and prefix.value: + ret.append(String.unquoted(prefix.value + separator + six.text_type(i))) + else: + ret.append(Number(i)) + + return List(ret, use_comma=True) + + +@ns.declare_alias('headings') +@ns.declare +def headers(frm=None, to=None): + if frm and to is None: + if isinstance(frm, String) and frm.value.lower() == 'all': + frm = 1 + to = 6 + else: + try: + to = int(getattr(frm, 'value', frm)) + except ValueError: + to = 6 + frm = 1 + else: + try: + frm = 1 if frm is None else int(getattr(frm, 'value', frm)) + except ValueError: + frm = 1 + try: + to = 6 if to is None else int(getattr(to, 'value', to)) + except ValueError: + to = 6 + ret = [String.unquoted('h' + six.text_type(i)) for i in range(frm, to + 1)] + return List(ret, use_comma=True) + + +@ns.declare +def nest(*arguments): + if isinstance(arguments[0], List): + lst = arguments[0] + elif isinstance(arguments[0], String): + lst = arguments[0].value.split(',') + else: + raise TypeError("Expected list or string, got %r" % (arguments[0],)) + + ret = [] + for s in lst: + if isinstance(s, String): + s = s.value + elif isinstance(s, six.string_types): + s = s + else: + raise TypeError("Expected string, got %r" % (s,)) + + s = s.strip() + if not s: + continue + + ret.append(s) + + for arg in arguments[1:]: + if isinstance(arg, List): + lst = arg + elif isinstance(arg, String): + lst = arg.value.split(',') + else: + raise TypeError("Expected list or string, got %r" % (arg,)) + + new_ret = [] + for s in lst: + if isinstance(s, String): + s = s.value + elif isinstance(s, six.string_types): + s = s + else: + raise TypeError("Expected string, got %r" % (s,)) + + s = s.strip() + if not s: + continue + + for r in ret: + if '&' in s: + new_ret.append(s.replace('&', r)) + else: + if not r or r[-1] in ('.', ':', '#'): + new_ret.append(r + s) + else: + new_ret.append(r + ' ' + s) + ret = new_ret + + ret = [String.unquoted(s) for s in sorted(set(ret))] + return List(ret, use_comma=True) + + +# This isn't actually from Compass, but it's just a shortcut for enumerate(). +# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through') +@ns.declare +def range_(frm, through=None): + if through is None: + through = frm + frm = 1 + return enumerate_(None, frm, through) + +# ------------------------------------------------------------------------------ +# Working with CSS constants + +OPPOSITE_POSITIONS = { + 'top': String.unquoted('bottom'), + 'bottom': String.unquoted('top'), + 'left': String.unquoted('right'), + 'right': String.unquoted('left'), + 'center': String.unquoted('center'), +} +DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')] + + +def _position(opposite, positions): + if positions is None: + positions = DEFAULT_POSITION + else: + positions = List.from_maybe(positions) + + ret = [] + for pos in positions: + if isinstance(pos, (String, six.string_types)): + pos_value = getattr(pos, 'value', pos) + if pos_value in OPPOSITE_POSITIONS: + if opposite: + ret.append(OPPOSITE_POSITIONS[pos_value]) + else: + ret.append(pos) + continue + elif pos_value == 'to': + # Gradient syntax keyword; leave alone + ret.append(pos) + continue + + elif isinstance(pos, Number): + if pos.is_simple_unit('%'): + if opposite: + ret.append(Number(100 - pos.value, '%')) + else: + ret.append(pos) + continue + elif pos.is_simple_unit('deg'): + # TODO support other angle types? + if opposite: + ret.append(Number((pos.value + 180) % 360, 'deg')) + else: + ret.append(pos) + continue + + if opposite: + log.warn("Can't find opposite for position %r" % (pos,)) + ret.append(pos) + + return List(ret, use_comma=False).maybe() + + +@ns.declare +def position(p): + return _position(False, p) + + +@ns.declare +def opposite_position(p): + return _position(True, p) + + +# ------------------------------------------------------------------------------ +# Math + +@ns.declare +def pi(): + return Number(math.pi) + + +@ns.declare +def e(): + return Number(math.e) + + +@ns.declare +def log_(number, base=None): + if not isinstance(number, Number): + raise TypeError("Expected number, got %r" % (number,)) + elif not number.is_unitless: + raise ValueError("Expected unitless number, got %r" % (number,)) + + if base is None: + pass + elif not isinstance(base, Number): + raise TypeError("Expected number, got %r" % (base,)) + elif not base.is_unitless: + raise ValueError("Expected unitless number, got %r" % (base,)) + + if base is None: + ret = math.log(number.value) + else: + ret = math.log(number.value, base.value) + + return Number(ret) + + +@ns.declare +def pow(number, exponent): + return number ** exponent + + +ns.set_function('sqrt', 1, Number.wrap_python_function(math.sqrt)) +ns.set_function('sin', 1, Number.wrap_python_function(math.sin)) +ns.set_function('cos', 1, Number.wrap_python_function(math.cos)) +ns.set_function('tan', 1, Number.wrap_python_function(math.tan)) + + +# ------------------------------------------------------------------------------ +# Fonts + +def _fonts_root(): + return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT + + +def _font_url(path, only_path=False, cache_buster=True, inline=False): + filepath = String.unquoted(path).value + file = None + FONTS_ROOT = _fonts_root() + if callable(FONTS_ROOT): + try: + _file, _storage = list(FONTS_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + elif inline: + file = _storage.open(_file) + else: + _path = os.path.join(FONTS_ROOT, filepath.strip('/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + elif inline: + file = open(_path, 'rb') + + BASE_URL = config.FONTS_URL or config.STATIC_URL + if file and inline: + font_type = None + if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value): + font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value + + try: + mime = FONT_TYPES[font_type] + except KeyError: + raise Exception('Could not determine font type for "%s"' % path.value) + + mime = FONT_TYPES.get(font_type) + if font_type == 'woff': + mime = 'application/font-woff' + elif font_type == 'eot': + mime = 'application/vnd.ms-fontobject' + url = make_data_url( + (mime if '/' in mime else 'font/%s' % mime), + file.read()) + file.close() + else: + url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/')) + if cache_buster and filetime != 'NA': + url = add_cache_buster(url, filetime) + + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) + + +def _font_files(args, inline): + if args == (): + return String.unquoted("") + + fonts = [] + args_len = len(args) + skip_next = False + for index, arg in enumerate(args): + if not skip_next: + font_type = args[index + 1] if args_len > (index + 1) else None + if font_type and font_type.value in FONT_TYPES: + skip_next = True + else: + if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value): + font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1]) + + if font_type.value in FONT_TYPES: + fonts.append(List([ + _font_url(arg, inline=inline), + Function(FONT_TYPES[font_type.value], 'format'), + ], use_comma=False)) + else: + raise Exception('Could not determine font type for "%s"' % arg.value) + else: + skip_next = False + + return List(fonts, separator=',') + + +@ns.declare +def font_url(path, only_path=False, cache_buster=True): + """ + Generates a path to an asset found relative to the project's font directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + return _font_url(path, only_path, cache_buster, False) + + +@ns.declare +def font_files(*args): + return _font_files(args, inline=False) + + +@ns.declare +def inline_font_files(*args): + return _font_files(args, inline=True) + + +# ------------------------------------------------------------------------------ +# External stylesheets + +@ns.declare +def stylesheet_url(path, only_path=False, cache_buster=True): + """ + Generates a path to an asset found relative to the project's css directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + filepath = String.unquoted(path).value + if callable(config.STATIC_ROOT): + try: + _file, _storage = list(config.STATIC_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + else: + _path = os.path.join(config.STATIC_ROOT, filepath.strip('/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + BASE_URL = config.STATIC_URL + + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + url = add_cache_buster(url, filetime) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) diff --git a/aip_site/vendor/pyscss/scss/extension/compass/images.py b/aip_site/vendor/pyscss/scss/extension/compass/images.py new file mode 100644 index 0000000..b2f33f1 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/images.py @@ -0,0 +1,289 @@ +"""Image utilities ported from Compass.""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import mimetypes +import os.path + +import six +from six.moves import xrange + +from . import CompassExtension +from .helpers import add_cache_buster +from scss import config +from scss.errors import SassMissingDependency +from scss.types import Color, List, Number, String, Url +from scss.util import getmtime, make_data_url, make_filename_hash +from scss.extension import Cache + +try: + from PIL import Image +except ImportError: + try: + import Image + except: + Image = None + +log = logging.getLogger(__name__) +ns = CompassExtension.namespace + + +def _images_root(): + return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT + + +def _assets_root(): + return config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') + + +def _get_cache(prefix): + return Cache((config.CACHE_ROOT or _assets_root(), prefix)) + + +def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None): + """ + src_color - a list of or a single color to be replaced by each corresponding dst_color colors + spacing - spaces to be added to the image + collapse_x, collapse_y - collapsable (layered) image of the given size (x, y) + """ + if inline or dst_color or spacing: + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + + filepath = String.unquoted(path).value + fileext = os.path.splitext(filepath)[1].lstrip('.').lower() + if mime_type: + mime_type = String.unquoted(mime_type).value + if not mime_type: + mime_type = mimetypes.guess_type(filepath)[0] + if not mime_type: + mime_type = 'image/%s' % fileext + path = None + IMAGES_ROOT = _images_root() + if callable(IMAGES_ROOT): + try: + _file, _storage = list(IMAGES_ROOT(filepath))[0] + except IndexError: + filetime = None + else: + filetime = getmtime(_file, _storage) + if filetime is None: + filetime = 'NA' + elif inline or dst_color or spacing: + path = _storage.open(_file) + else: + _path = os.path.join(IMAGES_ROOT.rstrip(os.sep), filepath.strip('\\/')) + filetime = getmtime(_path) + if filetime is None: + filetime = 'NA' + elif inline or dst_color or spacing: + path = open(_path, 'rb') + + BASE_URL = config.IMAGES_URL or config.STATIC_URL + if path: + dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v] + + src_color = Color.from_name('black') if src_color is None else src_color + src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)] + + len_colors = max(len(dst_colors), len(src_colors)) + dst_colors = (dst_colors * len_colors)[:len_colors] + src_colors = (src_colors * len_colors)[:len_colors] + + spacing = Number(0) if spacing is None else spacing + spacing = [int(Number(v).value) for v in List.from_maybe(spacing)] + spacing = (spacing * 4)[:4] + + file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace(os.sep, '_')) + key = (filetime, src_color, dst_color, spacing) + asset_file = file_name + '-' + make_filename_hash(key) + file_ext + ASSETS_ROOT = _assets_root() + asset_path = os.path.join(ASSETS_ROOT, asset_file) + + if os.path.exists(asset_path): + filepath = asset_file + BASE_URL = config.ASSETS_URL + if inline: + path = open(asset_path, 'rb') + url = make_data_url(mime_type, path.read()) + else: + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + filetime = getmtime(asset_path) + url = add_cache_buster(url, filetime) + else: + simply_process = False + image = None + + if fileext in ('cur',): + simply_process = True + else: + try: + image = Image.open(path) + except IOError: + if not collapse_x and not collapse_y and not dst_colors: + simply_process = True + + if simply_process: + if inline: + url = make_data_url(mime_type, path.read()) + else: + url = '%s%s' % (BASE_URL, filepath) + if cache_buster: + filetime = getmtime(asset_path) + url = add_cache_buster(url, filetime) + else: + width, height = collapse_x or image.size[0], collapse_y or image.size[1] + new_image = Image.new( + mode='RGBA', + size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]), + color=(0, 0, 0, 0) + ) + for i, dst_color in enumerate(dst_colors): + src_color = src_colors[i] + pixdata = image.load() + for _y in xrange(image.size[1]): + for _x in xrange(image.size[0]): + pixel = pixdata[_x, _y] + if pixel[:3] == src_color: + pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255]) + iwidth, iheight = image.size + if iwidth != width or iheight != height: + cy = 0 + while cy < iheight: + cx = 0 + while cx < iwidth: + cropped_image = image.crop((cx, cy, cx + width, cy + height)) + new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image) + cx += width + cy += height + else: + new_image.paste(image, (int(spacing[3]), int(spacing[0]))) + + if not inline: + try: + new_image.save(asset_path) + filepath = asset_file + BASE_URL = config.ASSETS_URL + if cache_buster: + filetime = getmtime(asset_path) + except IOError: + log.exception("Error while saving image") + inline = True # Retry inline version + url = os.path.join(config.ASSETS_URL.rstrip(os.sep), asset_file.lstrip(os.sep)) + if cache_buster: + url = add_cache_buster(url, filetime) + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url(mime_type, contents) + else: + url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('\\/')) + if cache_buster and filetime != 'NA': + url = add_cache_buster(url, filetime) + + if not os.sep == '/': + url = url.replace(os.sep, '/') + + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) + + +@ns.declare +def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): + """ + Embeds the contents of a file directly inside your stylesheet, eliminating + the need for another HTTP request. For small files such images or fonts, + this can be a performance benefit at the cost of a larger generated CSS + file. + """ + return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y) + + +@ns.declare +def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None): + """ + Generates a path to an asset found relative to the project's images + directory. + Passing a true value as the second argument will cause the only the path to + be returned instead of a `url()` function + """ + return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y) + + +@ns.declare +def image_width(image): + """ + Returns the width of the image found at the path supplied by `image` + relative to your project's images directory. + """ + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + + image_size_cache = _get_cache('image_size_cache') + + filepath = String.unquoted(image).value + path = None + try: + width = image_size_cache[filepath][0] + except KeyError: + width = 0 + IMAGES_ROOT = _images_root() + if callable(IMAGES_ROOT): + try: + _file, _storage = list(IMAGES_ROOT(filepath))[0] + except IndexError: + pass + else: + path = _storage.open(_file) + else: + _path = os.path.join(IMAGES_ROOT, filepath.strip(os.sep)) + if os.path.exists(_path): + path = open(_path, 'rb') + if path: + image = Image.open(path) + size = image.size + width = size[0] + image_size_cache[filepath] = size + return Number(width, 'px') + + +@ns.declare +def image_height(image): + """ + Returns the height of the image found at the path supplied by `image` + relative to your project's images directory. + """ + image_size_cache = _get_cache('image_size_cache') + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + filepath = String.unquoted(image).value + path = None + try: + height = image_size_cache[filepath][1] + except KeyError: + height = 0 + IMAGES_ROOT = _images_root() + if callable(IMAGES_ROOT): + try: + _file, _storage = list(IMAGES_ROOT(filepath))[0] + except IndexError: + pass + else: + path = _storage.open(_file) + else: + _path = os.path.join(IMAGES_ROOT, filepath.strip(os.sep)) + if os.path.exists(_path): + path = open(_path, 'rb') + if path: + image = Image.open(path) + size = image.size + height = size[1] + image_size_cache[filepath] = size + return Number(height, 'px') diff --git a/aip_site/vendor/pyscss/scss/extension/compass/layouts.py b/aip_site/vendor/pyscss/scss/extension/compass/layouts.py new file mode 100644 index 0000000..ae086ce --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/layouts.py @@ -0,0 +1,347 @@ +"""Functions used for generating packed CSS sprite maps. + +These are ported from the Binary Tree Bin Packing Algorithm: +http://codeincomplete.com/posts/2011/5/7/bin_packing/ +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +# Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors +# Copyright (c) 2013 German M. Bravo + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +class LayoutNode(object): + def __init__(self, x, y, w, h, down=None, right=None, used=False): + self.x = x + self.y = y + self.w = w + self.h = h + self.down = down + self.right = right + self.used = used + self.width = 0 + self.height = 0 + + @property + def area(self): + return self.width * self.height + + def __repr__(self): + return '<%s (%s, %s) [%sx%s]>' % (self.__class__.__name__, self.x, self.y, self.w, self.h) + + +class SpritesLayout(object): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): + self.num_blocks = len(blocks) + + if margin is None: + margin = [[0] * 4] * self.num_blocks + elif not isinstance(margin, (tuple, list)): + margin = [[margin] * 4] * self.num_blocks + elif not isinstance(margin[0], (tuple, list)): + margin = [margin] * self.num_blocks + + if padding is None: + padding = [[0] * 4] * self.num_blocks + elif not isinstance(padding, (tuple, list)): + padding = [[padding] * 4] * self.num_blocks + elif not isinstance(padding[0], (tuple, list)): + padding = [padding] * self.num_blocks + + if pmargin is None: + pmargin = [[0.0] * 4] * self.num_blocks + elif not isinstance(pmargin, (tuple, list)): + pmargin = [[pmargin] * 4] * self.num_blocks + elif not isinstance(pmargin[0], (tuple, list)): + pmargin = [pmargin] * self.num_blocks + + if ppadding is None: + ppadding = [[0.0] * 4] * self.num_blocks + elif not isinstance(ppadding, (tuple, list)): + ppadding = [[ppadding] * 4] * self.num_blocks + elif not isinstance(ppadding[0], (tuple, list)): + ppadding = [ppadding] * self.num_blocks + + self.blocks = tuple(( + b[0] + padding[i][3] + padding[i][1] + margin[i][3] + margin[i][1] + int(round(b[0] * (ppadding[i][3] + ppadding[i][1] + pmargin[i][3] + pmargin[i][1]))), + b[1] + padding[i][0] + padding[i][2] + margin[i][0] + margin[i][2] + int(round(b[1] * (ppadding[i][0] + ppadding[i][2] + pmargin[i][0] + pmargin[i][2]))), + b[0], + b[1], + i + ) for i, b in enumerate(blocks)) + + self.margin = margin + self.padding = padding + self.pmargin = pmargin + self.ppadding = ppadding + + +class PackedSpritesLayout(SpritesLayout): + @staticmethod + def MAXSIDE(a, b): + """maxside: Sort pack by maximum sides""" + return cmp(max(b[0], b[1]), max(a[0], a[1])) or cmp(min(b[0], b[1]), min(a[0], a[1])) or cmp(b[1], a[1]) or cmp(b[0], a[0]) + + @staticmethod + def WIDTH(a, b): + """width: Sort pack by width""" + return cmp(b[0], a[0]) or cmp(b[1], a[1]) + + @staticmethod + def HEIGHT(a, b): + """height: Sort pack by height""" + return cmp(b[1], a[1]) or cmp(b[0], a[0]) + + @staticmethod + def AREA(a, b): + """area: Sort pack by area""" + return cmp(b[0] * b[1], a[0] * a[1]) or cmp(b[1], a[1]) or cmp(b[0], a[0]) + + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, methods=None): + super(PackedSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + ratio = 0 + + if methods is None: + methods = (self.MAXSIDE, self.WIDTH, self.HEIGHT, self.AREA) + + for method in methods: + sorted_blocks = sorted( + self.blocks, + cmp=method, + ) + root = LayoutNode( + x=0, + y=0, + w=sorted_blocks[0][0] if sorted_blocks else 0, + h=sorted_blocks[0][1] if sorted_blocks else 0 + ) + + area = 0 + nodes = [None] * self.num_blocks + + for block in sorted_blocks: + w, h, width, height, i = block + node = self._findNode(root, w, h) + if node: + node = self._splitNode(node, w, h) + else: + root = self._growNode(root, w, h) + node = self._findNode(root, w, h) + if node: + node = self._splitNode(node, w, h) + else: + node = None + nodes[i] = node + node.width = width + node.height = height + area += node.area + + this_ratio = area / float(root.w * root.h) + # print method.__doc__, "%g%%" % (this_ratio * 100) + if ratio < this_ratio: + self.root = root + self.nodes = nodes + self.method = method + ratio = this_ratio + if ratio > 0.96: + break + # print self.method.__doc__, "%g%%" % (ratio * 100) + + def __iter__(self): + for i, node in enumerate(self.nodes): + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + cssw = node.width + padding[3] + padding[1] + int(round(node.width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = node.height + padding[0] + padding[2] + int(round(node.height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = node.x + margin[3] + int(round(node.width * pmargin[3])) + cssy = node.y + margin[0] + int(round(node.height * pmargin[0])) + x = cssx + padding[3] + int(round(node.width * ppadding[3])) + y = cssy + padding[0] + int(round(node.height * ppadding[0])) + yield x, y, node.width, node.height, cssx, cssy, cssw, cssh + + @property + def width(self): + return self.root.w + + @property + def height(self): + return self.root.h + + def _findNode(self, root, w, h): + if root.used: + return self._findNode(root.right, w, h) or self._findNode(root.down, w, h) + elif w <= root.w and h <= root.h: + return root + else: + return None + + def _splitNode(self, node, w, h): + node.used = True + node.down = LayoutNode( + x=node.x, + y=node.y + h, + w=node.w, + h=node.h - h + ) + node.right = LayoutNode( + x=node.x + w, + y=node.y, + w=node.w - w, + h=h + ) + return node + + def _growNode(self, root, w, h): + canGrowDown = w <= root.w + canGrowRight = h <= root.h + + shouldGrowRight = canGrowRight and (root.h >= root.w + w) # attempt to keep square-ish by growing right when height is much greater than width + shouldGrowDown = canGrowDown and (root.w >= root.h + h) # attempt to keep square-ish by growing down when width is much greater than height + + if shouldGrowRight: + return self._growRight(root, w, h) + elif shouldGrowDown: + return self._growDown(root, w, h) + elif canGrowRight: + return self._growRight(root, w, h) + elif canGrowDown: + return self._growDown(root, w, h) + else: + # need to ensure sensible root starting size to avoid this happening + assert False, "Blocks must be properly sorted!" + + def _growRight(self, root, w, h): + root = LayoutNode( + used=True, + x=0, + y=0, + w=root.w + w, + h=root.h, + down=root, + right=LayoutNode( + x=root.w, + y=0, + w=w, + h=root.h + ) + ) + return root + + def _growDown(self, root, w, h): + root = LayoutNode( + used=True, + x=0, + y=0, + w=root.w, + h=root.h + h, + down=LayoutNode( + x=0, + y=root.h, + w=root.w, + h=h + ), + right=root + ) + return root + + +class HorizontalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): + super(HorizontalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + self.width = sum(block[0] for block in self.blocks) + self.height = max(block[1] for block in self.blocks) + + if position is None: + position = [0.0] * self.num_blocks + elif not isinstance(position, (tuple, list)): + position = [position] * self.num_blocks + self.position = position + + def __iter__(self): + cx = 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + position = self.position[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x + cssy = int(round((self.height - cssh) * position)) # centered vertically + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) + + +class VerticalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None): + super(VerticalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + + self.width = max(block[0] for block in self.blocks) + self.height = sum(block[1] for block in self.blocks) + + if position is None: + position = [0.0] * self.num_blocks + elif not isinstance(position, (tuple, list)): + position = [position] * self.num_blocks + self.position = position + + def __iter__(self): + cy = 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + position = self.position[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = int(round((self.width - cssw) * position)) # centered horizontally + cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) + + +class DiagonalSpritesLayout(SpritesLayout): + def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None): + super(DiagonalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin) + self.width = sum(block[0] for block in self.blocks) + self.height = sum(block[1] for block in self.blocks) + + def __iter__(self): + cx, cy = 0, 0 + for i, block in enumerate(self.blocks): + w, h, width, height, i = block + margin, padding = self.margin[i], self.padding[i] + pmargin, ppadding = self.pmargin[i], self.ppadding[i] + cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding + cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding + cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x + cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y + x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding + y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding + yield x, y, width, height, cssx, cssy, cssw, cssh + cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1]))) + cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2]))) diff --git a/aip_site/vendor/pyscss/scss/extension/compass/sprites.py b/aip_site/vendor/pyscss/scss/extension/compass/sprites.py new file mode 100644 index 0000000..28ba4fb --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/compass/sprites.py @@ -0,0 +1,574 @@ +"""Functions used for generating CSS sprites. + +These are ported from the Compass sprite library: +http://compass-style.org/reference/compass/utilities/sprites/ +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +import six + +import glob +import logging +import os.path +import tempfile +import time +import sys + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + from PIL import Image +except ImportError: + try: + import Image + except: + Image = None + +from six.moves import xrange + +from . import CompassExtension +from .layouts import PackedSpritesLayout, HorizontalSpritesLayout, VerticalSpritesLayout, DiagonalSpritesLayout +from scss import config +from scss.errors import SassMissingDependency +from scss.types import Color, List, Number, String, Boolean +from scss.util import escape, getmtime, make_data_url, make_filename_hash +from scss.extension import Cache + +log = logging.getLogger(__name__) +ns = CompassExtension.namespace + +MAX_SPRITE_MAPS = 4096 +KEEP_SPRITE_MAPS = int(MAX_SPRITE_MAPS * 0.8) + + +def _assets_root(): + return config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') + + +def _get_cache(prefix): + return Cache((config.CACHE_ROOT or _assets_root(), prefix)) + + +# ------------------------------------------------------------------------------ +# Compass-like functionality for sprites and images + +def alpha_composite(im1, im2, offset=None, box=None, opacity=1): + im1size = im1.size + im2size = im2.size + if offset is None: + offset = (0, 0) + if box is None: + box = (0, 0) + im2size + o1x, o1y = offset + o2x, o2y, o2w, o2h = box + width = o2w - o2x + height = o2h - o2y + im1_data = im1.load() + im2_data = im2.load() + for y in xrange(height): + for x in xrange(width): + pos1 = o1x + x, o1y + y + if pos1[0] >= im1size[0] or pos1[1] >= im1size[1]: + continue + pos2 = o2x + x, o2y + y + if pos2[0] >= im2size[0] or pos2[1] >= im2size[1]: + continue + dr, dg, db, da = im1_data[pos1] + sr, sg, sb, sa = im2_data[pos2] + da /= 255.0 + sa /= 255.0 + sa *= opacity + ida = da * (1 - sa) + oa = (sa + ida) + if oa: + pixel = ( + int(round((sr * sa + dr * ida)) / oa), + int(round((sg * sa + dg * ida)) / oa), + int(round((sb * sa + db * ida)) / oa), + int(round(255 * oa)) + ) + else: + pixel = (0, 0, 0, 0) + im1_data[pos1] = pixel + return im1 + + +@ns.declare +def sprite_map(g, **kwargs): + """ + Generates a sprite map from the files matching the glob pattern. + Uses the keyword-style arguments passed in to control the placement. + + $direction - Sprite map layout. Can be `vertical` (default), `horizontal`, `diagonal` or `smart`. + + $position - For `horizontal` and `vertical` directions, the position of the sprite. (defaults to `0`) + $-position - Position of a given sprite. + + $padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`) + $-padding, $-spacing - Padding for a given sprite. + + $dst-color - Together with `$src-color`, forms a map of source colors to be converted to destiny colors (same index of `$src-color` changed to `$dst-color`). + $-dst-color - Destiny colors for a given sprite. (defaults to `$dst-color`) + + $src-color - Selects source colors to be converted to the corresponding destiny colors. (defaults to `black`) + $-dst-color - Source colors for a given sprite. (defaults to `$src-color`) + + $collapse - Collapses every image in the sprite map to a fixed size (`x` and `y`). + $collapse-x - Collapses a size for `x`. + $collapse-y - Collapses a size for `y`. + """ + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + + sprite_maps = _get_cache('sprite_maps') + + now_time = time.time() + + globs = String(g, quotes=None).value + globs = sorted(g.strip() for g in globs.split(',')) + + _k_ = ','.join(globs) + + files = None + rfiles = None + tfiles = None + map_name = None + + if _k_ in sprite_maps: + sprite_maps[_k_]['*'] = now_time + else: + files = [] + rfiles = [] + tfiles = [] + for _glob in globs: + if '..' not in _glob: # Protect against going to prohibited places... + if callable(config.STATIC_ROOT): + _glob_path = _glob + _rfiles = _files = sorted(config.STATIC_ROOT(_glob)) + else: + _glob_path = os.path.join(config.STATIC_ROOT, _glob) + _files = glob.glob(_glob_path) + _files = sorted((f, None) for f in _files) + _rfiles = [(rf[len(config.STATIC_ROOT):], s) for rf, s in _files] + if _files: + files.extend(_files) + rfiles.extend(_rfiles) + base_name = os.path.normpath(os.path.dirname(_glob)).replace('\\', '_').replace('/', '_') + _map_name, _, _map_type = base_name.partition('.') + if _map_type: + _map_type += '-' + if not map_name: + map_name = _map_name + tfiles.extend([_map_type] * len(_files)) + else: + glob_path = _glob_path + + if files is not None: + if not files: + log.error("Nothing found at '%s'", glob_path) + return String.unquoted('') + + key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] + key = map_name + '-' + make_filename_hash(key) + asset_file = key + '.png' + ASSETS_ROOT = _assets_root() + asset_path = os.path.join(ASSETS_ROOT, asset_file) + cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, asset_file + '.cache') + + inline = Boolean(kwargs.get('inline', False)) + + sprite_map = None + asset = None + file_asset = None + inline_asset = None + if os.path.exists(asset_path) or inline: + try: + save_time, file_asset, inline_asset, sprite_map, sizes = pickle.load(open(cache_path)) + if file_asset: + sprite_maps[file_asset.render()] = sprite_map + if inline_asset: + sprite_maps[inline_asset.render()] = sprite_map + if inline: + asset = inline_asset + else: + asset = file_asset + except: + pass + + if sprite_map: + for file_, storage in files: + _time = getmtime(file_, storage) + if save_time < _time: + if _time > now_time: + log.warning("File '%s' has a date in the future (cache ignored)" % file_) + sprite_map = None # Invalidate cached sprite map + break + + if sprite_map is None or asset is None: + cache_buster = Boolean(kwargs.get('cache_buster', True)) + direction = String.unquoted(kwargs.get('direction', config.SPRTE_MAP_DIRECTION)).value + repeat = String.unquoted(kwargs.get('repeat', 'no-repeat')).value + collapse = kwargs.get('collapse', Number(0)) + if isinstance(collapse, List): + collapse_x = int(Number(collapse[0]).value) + collapse_y = int(Number(collapse[-1]).value) + else: + collapse_x = collapse_y = int(Number(collapse).value) + if 'collapse_x' in kwargs: + collapse_x = int(Number(kwargs['collapse_x']).value) + if 'collapse_y' in kwargs: + collapse_y = int(Number(kwargs['collapse_y']).value) + + position = Number(kwargs.get('position', 0)) + if not position.is_simple_unit('%') and position.value > 1: + position = position.value / 100.0 + else: + position = position.value + if position < 0: + position = 0.0 + elif position > 1: + position = 1.0 + + padding = kwargs.get('padding', kwargs.get('spacing', Number(0))) + padding = [int(Number(v).value) for v in List.from_maybe(padding)] + padding = (padding * 4)[:4] + + dst_colors = kwargs.get('dst_color') + dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_colors) if v] + src_colors = kwargs.get('src_color', Color.from_name('black')) + src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_colors)] + len_colors = max(len(dst_colors), len(src_colors)) + dst_colors = (dst_colors * len_colors)[:len_colors] + src_colors = (src_colors * len_colors)[:len_colors] + + def images(f=lambda x: x): + for file_, storage in f(files): + if storage is not None: + _file = storage.open(file_) + else: + _file = file_ + _image = Image.open(_file) + yield _image + + names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files) + tnames = tuple(tfiles[i] + n for i, n in enumerate(names)) + + has_dst_colors = False + all_dst_colors = [] + all_src_colors = [] + all_positions = [] + all_paddings = [] + + for name in names: + name = name.replace('-', '_') + + _position = kwargs.get(name + '_position') + if _position is None: + _position = position + else: + _position = Number(_position) + if not _position.is_simple_unit('%') and _position.value > 1: + _position = _position.value / 100.0 + else: + _position = _position.value + if _position < 0: + _position = 0.0 + elif _position > 1: + _position = 1.0 + all_positions.append(_position) + + _padding = kwargs.get(name + '_padding', kwargs.get(name + '_spacing')) + if _padding is None: + _padding = padding + else: + _padding = [int(Number(v).value) for v in List.from_maybe(_padding)] + _padding = (_padding * 4)[:4] + all_paddings.append(_padding) + + _dst_colors = kwargs.get(name + '_dst_color') + if _dst_colors is None: + _dst_colors = dst_colors + if dst_colors: + has_dst_colors = True + else: + has_dst_colors = True + _dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(_dst_colors) if v] + _src_colors = kwargs.get(name + '_src_color', Color.from_name('black')) + if _src_colors is None: + _src_colors = src_colors + else: + _src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(_src_colors)] + _len_colors = max(len(_dst_colors), len(_src_colors)) + _dst_colors = (_dst_colors * _len_colors)[:_len_colors] + _src_colors = (_src_colors * _len_colors)[:_len_colors] + all_dst_colors.append(_dst_colors) + all_src_colors.append(_src_colors) + + sizes = tuple((collapse_x or i.size[0], collapse_y or i.size[1]) for i in images()) + + if direction == 'horizontal': + layout = HorizontalSpritesLayout(sizes, all_paddings, position=all_positions) + elif direction == 'vertical': + layout = VerticalSpritesLayout(sizes, all_paddings, position=all_positions) + elif direction == 'diagonal': + layout = DiagonalSpritesLayout(sizes, all_paddings) + elif direction == 'smart': + layout = PackedSpritesLayout(sizes, all_paddings) + else: + raise Exception("Invalid direction %r" % (direction,)) + layout_positions = list(layout) + + new_image = Image.new( + mode='RGBA', + size=(layout.width, layout.height), + color=(0, 0, 0, 0) + ) + + useless_dst_color = has_dst_colors + + offsets_x = [] + offsets_y = [] + for i, image in enumerate(images()): + x, y, width, height, cssx, cssy, cssw, cssh = layout_positions[i] + iwidth, iheight = image.size + + if has_dst_colors: + pixdata = image.load() + for _y in xrange(iheight): + for _x in xrange(iwidth): + pixel = pixdata[_x, _y] + a = pixel[3] if len(pixel) == 4 else 255 + if a: + rgb = pixel[:3] + for j, dst_color in enumerate(all_dst_colors[i]): + if rgb == all_src_colors[i][j]: + new_color = tuple([int(c) for c in dst_color] + [a]) + if pixel != new_color: + pixdata[_x, _y] = new_color + useless_dst_color = False + break + + if iwidth != width or iheight != height: + cy = 0 + while cy < iheight: + cx = 0 + while cx < iwidth: + new_image = alpha_composite(new_image, image, (x, y), (cx, cy, cx + width, cy + height)) + cx += width + cy += height + else: + new_image.paste(image, (x, y)) + offsets_x.append(cssx) + offsets_y.append(cssy) + + if useless_dst_color: + log.warning("Useless use of $dst-color in sprite map for files at '%s' (never used for)" % glob_path) + + filetime = int(now_time) + + if not inline: + try: + new_image.save(asset_path) + url = '%s%s' % (config.ASSETS_URL, asset_file) + if cache_buster: + url += '?_=%s' % filetime + except IOError: + log.exception("Error while saving image") + inline = True + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + mime_type = 'image/png' + url = make_data_url(mime_type, contents) + + url = 'url(%s)' % escape(url) + if inline: + asset = inline_asset = List([String.unquoted(url), String.unquoted(repeat)]) + else: + asset = file_asset = List([String.unquoted(url), String.unquoted(repeat)]) + + # Add the new object: + sprite_map = dict(zip(tnames, zip(sizes, rfiles, offsets_x, offsets_y))) + sprite_map['*'] = now_time + sprite_map['*f*'] = asset_file + sprite_map['*k*'] = key + sprite_map['*n*'] = map_name + sprite_map['*t*'] = filetime + + sizes = zip(files, sizes) + cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) + pickle.dump((now_time, file_asset, inline_asset, sprite_map, sizes), cache_tmp) + cache_tmp.close() + if sys.platform == 'win32' and os.path.isfile(cache_path): + # on windows, cannot rename a file to a path that matches + # an existing file, we have to remove it first + os.remove(cache_path) + os.rename(cache_tmp.name, cache_path) + + # Use the sorted list to remove older elements (keep only 500 objects): + if len(sprite_maps) > MAX_SPRITE_MAPS: + for a in sorted(sprite_maps, key=lambda a: sprite_maps[a]['*'], reverse=True)[KEEP_SPRITE_MAPS:]: + del sprite_maps[a] + log.warning("Exceeded maximum number of sprite maps (%s)" % MAX_SPRITE_MAPS) + sprite_maps[asset.render()] = sprite_map + image_size_cache = _get_cache('image_size_cache') + for file_, size in sizes: + image_size_cache[file_] = size + # TODO this sometimes returns an empty list, or is never assigned to + return asset + + +@ns.declare +def sprite_map_name(map): + """ + Returns the name of a sprite map The name is derived from the folder than + contains the sprites. + """ + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + if sprite_map: + return String.unquoted(sprite_map['*n*']) + return String.unquoted('') + + +@ns.declare +def sprite_file(map, sprite): + """ + Returns the relative path (from the images directory) to the original file + used when construction the sprite. This is suitable for passing to the + image_width and image_height helpers. + """ + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + sprite_name = String.unquoted(sprite).value + sprite = sprite_map and sprite_map.get(sprite_name) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + elif not sprite: + log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True}) + if sprite: + return String(sprite[1][0]) + return String.unquoted('') + + +@ns.declare_alias('sprite-names') +@ns.declare +def sprites(map, remove_suffix=False): + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map, {}) + return List([String.unquoted(s) for s in sorted(set(s.rsplit('-', 1)[0] if remove_suffix else s for s in sprite_map if not s.startswith('*')))]) + + +@ns.declare +def sprite_classes(map): + return sprites(map, True) + + +@ns.declare +def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True): + """ + Returns the image and background position for use in a single shorthand + property + """ + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + sprite_name = String.unquoted(sprite).value + sprite = sprite_map and sprite_map.get(sprite_name) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + elif not sprite: + log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True}) + if sprite: + url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*']) + if cache_buster: + url += '?_=%s' % sprite_map['*t*'] + x = Number(offset_x or 0, 'px') + y = Number(offset_y or 0, 'px') + if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'): + x -= Number(sprite[2], 'px') + if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'): + y -= Number(sprite[3], 'px') + url = "url(%s)" % escape(url) + return List([String.unquoted(url), x, y]) + return List([Number(0), Number(0)]) + + +@ns.declare +def sprite_url(map, cache_buster=True): + """ + Returns a url to the sprite image. + """ + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + if sprite_map: + url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*']) + if cache_buster: + url += '?_=%s' % sprite_map['*t*'] + url = "url(%s)" % escape(url) + return String.unquoted(url) + return String.unquoted('') + + +@ns.declare +def has_sprite(map, sprite): + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + sprite_name = String.unquoted(sprite).value + sprite = sprite_map and sprite_map.get(sprite_name) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + return Boolean(bool(sprite)) + + +@ns.declare +def sprite_position(map, sprite, offset_x=None, offset_y=None): + """ + Returns the position for the original image in the sprite. + This is suitable for use as a value to background-position. + """ + map = map.render() + sprite_maps = _get_cache('sprite_maps') + sprite_map = sprite_maps.get(map) + sprite_name = String.unquoted(sprite).value + sprite = sprite_map and sprite_map.get(sprite_name) + if not sprite_map: + log.error("No sprite map found: %s", map, extra={'stack': True}) + elif not sprite: + log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True}) + if sprite: + x = None + if offset_x is not None and not isinstance(offset_x, Number): + x = offset_x + if not x or x.value not in ('left', 'right', 'center'): + if x: + offset_x = None + x = Number(offset_x or 0, 'px') + if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'): + x -= Number(sprite[2], 'px') + y = None + if offset_y is not None and not isinstance(offset_y, Number): + y = offset_y + if not y or y.value not in ('top', 'bottom', 'center'): + if y: + offset_y = None + y = Number(offset_y or 0, 'px') + if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'): + y -= Number(sprite[3], 'px') + return List([x, y]) + return List([Number(0), Number(0)]) diff --git a/aip_site/vendor/pyscss/scss/extension/core.py b/aip_site/vendor/pyscss/scss/extension/core.py new file mode 100644 index 0000000..5c7e765 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/core.py @@ -0,0 +1,943 @@ +"""Extension for built-in Sass functionality.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from itertools import product +import math +import os.path +from pathlib import PurePosixPath + +from six.moves import xrange + +from scss.extension import Extension +from scss.namespace import Namespace +from scss.source import SourceFile +from scss.types import ( + Arglist, Boolean, Color, List, Null, Number, String, Map, expect_type) + + +class CoreExtension(Extension): + name = 'core' + namespace = Namespace() + + def handle_import(self, name, compilation, rule): + """Implementation of the core Sass import mechanism, which just looks + for files on disk. + """ + # TODO this is all not terribly well-specified by Sass. at worst, + # it's unclear how far "upwards" we should be allowed to go. but i'm + # also a little fuzzy on e.g. how relative imports work from within a + # file that's not actually in the search path. + # TODO i think with the new origin semantics, i've made it possible to + # import relative to the current file even if the current file isn't + # anywhere in the search path. is that right? + path = PurePosixPath(name) + + search_exts = list(compilation.compiler.dynamic_extensions) + if path.suffix and path.suffix in search_exts: + basename = path.stem + else: + basename = path.name + relative_to = path.parent + search_path = [] # tuple of (origin, start_from) + if relative_to.is_absolute(): + relative_to = PurePosixPath(*relative_to.parts[1:]) + elif rule.source_file.origin: + # Search relative to the current file first, only if not doing an + # absolute import + search_path.append(( + rule.source_file.origin, + rule.source_file.relpath.parent / relative_to, + )) + search_path.extend( + (origin, relative_to) + for origin in compilation.compiler.search_path + ) + + for prefix, suffix in product(('_', ''), search_exts): + filename = prefix + basename + suffix + for origin, relative_to in search_path: + relpath = relative_to / filename + # Lexically (ignoring symlinks!) eliminate .. from the part + # of the path that exists within Sass-space. pathlib + # deliberately doesn't do this, but os.path does. + relpath = PurePosixPath(os.path.normpath(str(relpath))) + + if rule.source_file.key == (origin, relpath): + # Avoid self-import + # TODO is this what ruby does? + continue + + path = origin / relpath + if not path.exists(): + continue + + # All good! + # TODO if this file has already been imported, we'll do the + # source preparation twice. make it lazy. + return SourceFile.read(origin, relpath) + + +# Alias to make the below declarations less noisy +ns = CoreExtension.namespace + + +# ------------------------------------------------------------------------------ +# Color creation + +def _interpret_percentage(n, relto=1., clamp=True): + expect_type(n, Number, unit='%') + + if n.is_unitless: + ret = n.value / relto + else: + ret = n.value / 100 + + if clamp: + if ret < 0: + return 0 + elif ret > 1: + return 1 + + return ret + + +@ns.declare +def rgba(r, g, b, a): + r = _interpret_percentage(r, relto=255) + g = _interpret_percentage(g, relto=255) + b = _interpret_percentage(b, relto=255) + a = _interpret_percentage(a, relto=1) + + return Color.from_rgb(r, g, b, a) + + +@ns.declare +def rgb(r, g, b, type='rgb'): + return rgba(r, g, b, Number(1.0)) + + +@ns.declare +def rgba_(color, a=None): + if a is None: + alpha = 1 + else: + alpha = _interpret_percentage(a) + + return Color.from_rgb(*color.rgba[:3], alpha=alpha) + + +@ns.declare +def rgb_(color): + return rgba_(color, a=Number(1)) + + +@ns.declare +def hsla(h, s, l, a): + return Color.from_hsl( + h.value / 360 % 1, + # Ruby sass treats plain numbers for saturation and lightness as though + # they were percentages, just without the % + _interpret_percentage(s, relto=100), + _interpret_percentage(l, relto=100), + alpha=a.value, + ) + + +@ns.declare +def hsl(h, s, l): + return hsla(h, s, l, Number(1)) + + +@ns.declare +def hsla_(color, a=None): + return rgba_(color, a) + + +@ns.declare +def hsl_(color): + return rgba_(color, a=Number(1)) + + +@ns.declare +def mix(color1, color2, weight=Number(50, "%")): + """ + Mixes together two colors. Specifically, takes the average of each of the + RGB components, optionally weighted by the given percentage. + The opacity of the colors is also considered when weighting the components. + + Specifically, takes the average of each of the RGB components, + optionally weighted by the given percentage. + The opacity of the colors is also considered when weighting the components. + + The weight specifies the amount of the first color that should be included + in the returned color. + 50%, means that half the first color + and half the second color should be used. + 25% means that a quarter of the first color + and three quarters of the second color should be used. + + For example: + + mix(#f00, #00f) => #7f007f + mix(#f00, #00f, 25%) => #3f00bf + mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75) + """ + # This algorithm factors in both the user-provided weight + # and the difference between the alpha values of the two colors + # to decide how to perform the weighted average of the two RGB values. + # + # It works by first normalizing both parameters to be within [-1, 1], + # where 1 indicates "only use color1", -1 indicates "only use color 0", + # and all values in between indicated a proportionately weighted average. + # + # Once we have the normalized variables w and a, + # we apply the formula (w + a)/(1 + w*a) + # to get the combined weight (in [-1, 1]) of color1. + # This formula has two especially nice properties: + # + # * When either w or a are -1 or 1, the combined weight is also that + # number (cases where w * a == -1 are undefined, and handled as a + # special case). + # + # * When a is 0, the combined weight is w, and vice versa + # + # Finally, the weight of color1 is renormalized to be within [0, 1] + # and the weight of color2 is given by 1 minus the weight of color1. + # + # Algorithm from the Sass project: http://sass-lang.com/ + + p = _interpret_percentage(weight) + + # Scale weight to [-1, 1] + w = p * 2 - 1 + # Compute difference in alpha channels + a = color1.alpha - color2.alpha + + # Weight of first color + if w * a == -1: + # Avoid zero-div case + scaled_weight1 = w + else: + scaled_weight1 = (w + a) / (1 + w * a) + + # Unscale back to [0, 1] and get the weight of the other color + w1 = (scaled_weight1 + 1) / 2 + w2 = 1 - w1 + + # Do the scaling. Note that alpha isn't scaled by alpha, as that wouldn't + # make much sense; it uses the original untwiddled weight, p. + channels = [ + ch1 * w1 + ch2 * w2 + for (ch1, ch2) in zip(color1.rgba[:3], color2.rgba[:3])] + alpha = color1.alpha * p + color2.alpha * (1 - p) + return Color.from_rgb(*channels, alpha=alpha) + + +# ------------------------------------------------------------------------------ +# Color inspection + +@ns.declare +def red(color): + r, g, b, a = color.rgba + return Number(r * 255) + + +@ns.declare +def green(color): + r, g, b, a = color.rgba + return Number(g * 255) + + +@ns.declare +def blue(color): + r, g, b, a = color.rgba + return Number(b * 255) + + +@ns.declare_alias('opacity') +@ns.declare +def alpha(color): + return Number(color.alpha) + + +@ns.declare +def hue(color): + h, s, l = color.hsl + return Number(h * 360, "deg") + + +@ns.declare +def saturation(color): + h, s, l = color.hsl + return Number(s * 100, "%") + + +@ns.declare +def lightness(color): + h, s, l = color.hsl + return Number(l * 100, "%") + + +@ns.declare +def ie_hex_str(color): + c = Color(color).value + return String("#{3:02X}{0:02X}{1:02X}{2:02X}".format( + int(round(c[0])), + int(round(c[1])), + int(round(c[2])), + int(round(c[3] * 255)), + )) + + +# ------------------------------------------------------------------------------ +# Color modification + +@ns.declare_alias('fade-in') +@ns.declare_alias('fadein') +@ns.declare +def opacify(color, amount): + r, g, b, a = color.rgba + if amount.is_simple_unit('%'): + amt = amount.value / 100 + else: + amt = amount.value + return Color.from_rgb( + r, g, b, + alpha=a + amt) + + +@ns.declare_alias('fade-out') +@ns.declare_alias('fadeout') +@ns.declare +def transparentize(color, amount): + r, g, b, a = color.rgba + if amount.is_simple_unit('%'): + amt = amount.value / 100 + else: + amt = amount.value + return Color.from_rgb( + r, g, b, + alpha=a - amt) + + +@ns.declare +def lighten(color, amount): + return adjust_color(color, lightness=amount) + + +@ns.declare +def darken(color, amount): + return adjust_color(color, lightness=-amount) + + +@ns.declare +def saturate(color, amount): + return adjust_color(color, saturation=amount) + + +@ns.declare +def desaturate(color, amount): + return adjust_color(color, saturation=-amount) + + +@ns.declare +def greyscale(color): + h, s, l = color.hsl + return Color.from_hsl(h, 0, l, alpha=color.alpha) + + +@ns.declare +def grayscale(color): + if isinstance(color, Number): + # grayscale(n) and grayscale(n%) are CSS3 filters and should be left + # intact, but only when using the "a" spelling + return String.unquoted("grayscale(%s)" % (color.render(),)) + else: + return greyscale(color) + + +@ns.declare_alias('spin') +@ns.declare +def adjust_hue(color, degrees): + h, s, l = color.hsl + delta = degrees.value / 360 + return Color.from_hsl((h + delta) % 1, s, l, alpha=color.alpha) + + +@ns.declare +def complement(color): + h, s, l = color.hsl + return Color.from_hsl((h + 0.5) % 1, s, l, alpha=color.alpha) + + +@ns.declare +def invert(color): + """Returns the inverse (negative) of a color. The red, green, and blue + values are inverted, while the opacity is left alone. + """ + if isinstance(color, Number): + # invert(n) and invert(n%) are CSS3 filters and should be left + # intact + return String.unquoted("invert(%s)" % (color.render(),)) + + expect_type(color, Color) + r, g, b, a = color.rgba + return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a) + + +@ns.declare +def adjust_lightness(color, amount): + return adjust_color(color, lightness=amount) + + +@ns.declare +def adjust_saturation(color, amount): + return adjust_color(color, saturation=amount) + + +@ns.declare +def scale_lightness(color, amount): + return scale_color(color, lightness=amount) + + +@ns.declare +def scale_saturation(color, amount): + return scale_color(color, saturation=amount) + + +@ns.declare +def adjust_color( + color, red=None, green=None, blue=None, + hue=None, saturation=None, lightness=None, alpha=None): + do_rgb = red or green or blue + do_hsl = hue or saturation or lightness + if do_rgb and do_hsl: + raise ValueError( + "Can't adjust both RGB and HSL channels at the same time") + + zero = Number(0) + a = color.alpha + (alpha or zero).value + + if do_rgb: + r, g, b = color.rgba[:3] + channels = [ + current + (adjustment or zero).value / 255 + for (current, adjustment) in zip(color.rgba, (red, green, blue))] + return Color.from_rgb(*channels, alpha=a) + + else: + h, s, l = color.hsl + h = (h + (hue or zero).value / 360) % 1 + s += _interpret_percentage(saturation or zero, relto=100, clamp=False) + l += _interpret_percentage(lightness or zero, relto=100, clamp=False) + return Color.from_hsl(h, s, l, a) + + +def _scale_channel(channel, scaleby): + if scaleby is None: + return channel + + expect_type(scaleby, Number) + if not scaleby.is_simple_unit('%'): + raise ValueError("Expected percentage, got %r" % (scaleby,)) + + factor = scaleby.value / 100 + if factor > 0: + # Add x% of the remaining range, up to 1 + return channel + (1 - channel) * factor + else: + # Subtract x% of the existing channel. We add here because the factor + # is already negative + return channel * (1 + factor) + + +@ns.declare +def scale_color( + color, red=None, green=None, blue=None, + saturation=None, lightness=None, alpha=None): + do_rgb = red or green or blue + do_hsl = saturation or lightness + if do_rgb and do_hsl: + raise ValueError( + "Can't scale both RGB and HSL channels at the same time") + + scaled_alpha = _scale_channel(color.alpha, alpha) + + if do_rgb: + channels = [ + _scale_channel(channel, scaleby) + for channel, scaleby in zip(color.rgba, (red, green, blue))] + return Color.from_rgb(*channels, alpha=scaled_alpha) + + else: + channels = [ + _scale_channel(channel, scaleby) + for channel, scaleby + in zip(color.hsl, (None, saturation, lightness))] + return Color.from_hsl(*channels, alpha=scaled_alpha) + + +@ns.declare +def change_color( + color, red=None, green=None, blue=None, + hue=None, saturation=None, lightness=None, alpha=None): + do_rgb = red or green or blue + do_hsl = hue or saturation or lightness + if do_rgb and do_hsl: + raise ValueError( + "Can't change both RGB and HSL channels at the same time") + + if alpha is None: + alpha = color.alpha + else: + alpha = alpha.value + + if do_rgb: + channels = list(color.rgba[:3]) + if red: + channels[0] = _interpret_percentage(red, relto=255) + if green: + channels[1] = _interpret_percentage(green, relto=255) + if blue: + channels[2] = _interpret_percentage(blue, relto=255) + + return Color.from_rgb(*channels, alpha=alpha) + + else: + channels = list(color.hsl) + if hue: + expect_type(hue, Number, unit=None) + channels[0] = (hue.value / 360) % 1 + # Ruby sass treats plain numbers for saturation and lightness as though + # they were percentages, just without the % + if saturation: + channels[1] = _interpret_percentage(saturation, relto=100) + if lightness: + channels[2] = _interpret_percentage(lightness, relto=100) + + return Color.from_hsl(*channels, alpha=alpha) + + +# ------------------------------------------------------------------------------ +# String functions + +@ns.declare_alias('e') +@ns.declare_alias('escape') +@ns.declare +def unquote(*args): + arg = List.from_maybe_starargs(args).maybe() + + if isinstance(arg, String): + return String(arg.value, quotes=None) + else: + return String(arg.render(), quotes=None) + + +@ns.declare +def quote(*args): + arg = List.from_maybe_starargs(args).maybe() + + if isinstance(arg, String): + return String(arg.value, quotes='"') + else: + return String(arg.render(), quotes='"') + + +@ns.declare +def str_length(string): + expect_type(string, String) + + # nb: can't use `len(string)`, because that gives the Sass list length, + # which is 1 + return Number(len(string.value)) + + +# TODO this and several others should probably also require integers +# TODO and assert that the indexes are valid +@ns.declare +def str_insert(string, insert, index): + expect_type(string, String) + expect_type(insert, String) + expect_type(index, Number, unit=None) + + py_index = index.to_python_index(len(string.value), check_bounds=False) + return String( + string.value[:py_index] + insert.value + string.value[py_index:], + quotes=string.quotes) + + +@ns.declare +def str_index(string, substring): + expect_type(string, String) + expect_type(substring, String) + + # 1-based indexing, with 0 for failure + return Number(string.value.find(substring.value) + 1) + + +@ns.declare +def str_slice(string, start_at, end_at=None): + expect_type(string, String) + expect_type(start_at, Number, unit=None) + + if int(start_at) == 0: + py_start_at = 0 + else: + py_start_at = start_at.to_python_index(len(string.value)) + + if end_at is None or int(end_at) > len(string.value): + py_end_at = None + else: + expect_type(end_at, Number, unit=None) + # Endpoint is inclusive, unlike Python + py_end_at = end_at.to_python_index(len(string.value)) + 1 + + return String( + string.value[py_start_at:py_end_at], + quotes=string.quotes) + + +@ns.declare +def to_upper_case(string): + expect_type(string, String) + + return String(string.value.upper(), quotes=string.quotes) + + +@ns.declare +def to_lower_case(string): + expect_type(string, String) + + return String(string.value.lower(), quotes=string.quotes) + + +# ------------------------------------------------------------------------------ +# Number functions + +@ns.declare +def percentage(value): + expect_type(value, Number, unit=None) + return value * Number(100, unit='%') + + +ns.set_function('abs', 1, Number.wrap_python_function(abs)) +ns.set_function('round', 1, Number.wrap_python_function(round)) +ns.set_function('ceil', 1, Number.wrap_python_function(math.ceil)) +ns.set_function('floor', 1, Number.wrap_python_function(math.floor)) + + +# ------------------------------------------------------------------------------ +# List functions + +# TODO get the compass bit outta here +@ns.declare_alias('-compass-list-size') +@ns.declare +def length(*lst): + if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): + lst = lst[0] + return Number(len(lst)) + + +@ns.declare +def set_nth(list, n, value): + expect_type(n, Number, unit=None) + + py_n = n.to_python_index(len(list)) + return List( + tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]), + use_comma=list.use_comma) + + +# TODO get the compass bit outta here +@ns.declare_alias('-compass-nth') +@ns.declare +def nth(lst, n): + """Return the nth item in the list.""" + expect_type(n, (String, Number), unit=None) + + if isinstance(n, String): + if n.value.lower() == 'first': + i = 0 + elif n.value.lower() == 'last': + i = -1 + else: + raise ValueError("Invalid index %r" % (n,)) + else: + # DEVIATION: nth treats lists as circular lists + i = n.to_python_index(len(lst), circular=True) + + return lst[i] + + +@ns.declare +def join(lst1, lst2, separator=String.unquoted('auto')): + expect_type(separator, String) + + ret = [] + ret.extend(List.from_maybe(lst1)) + ret.extend(List.from_maybe(lst2)) + + if separator.value == 'comma': + use_comma = True + elif separator.value == 'space': + use_comma = False + elif separator.value == 'auto': + # The Sass docs are slightly misleading here, but the algorithm is: use + # the delimiter from the first list that has at least 2 items, or + # default to spaces. + if len(lst1) > 1: + use_comma = lst1.use_comma + elif len(lst2) > 1: + use_comma = lst2.use_comma + else: + use_comma = False + else: + raise ValueError("separator for join() must be comma, space, or auto") + + return List(ret, use_comma=use_comma) + + +@ns.declare +def min_(*lst): + if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): + lst = lst[0] + return min(lst) + + +@ns.declare +def max_(*lst): + if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): + lst = lst[0] + return max(lst) + + +@ns.declare +def append(lst, val, separator=String.unquoted('auto')): + expect_type(separator, String) + + ret = [] + ret.extend(List.from_maybe(lst)) + ret.append(val) + + separator = separator.value + if separator == 'comma': + use_comma = True + elif separator == 'space': + use_comma = False + elif separator == 'auto': + if len(lst) < 2: + use_comma = False + else: + use_comma = lst.use_comma + else: + raise ValueError('Separator must be auto, comma, or space') + + return List(ret, use_comma=use_comma) + + +@ns.declare +def index(lst, val): + for i in xrange(len(lst)): + if lst.value[i] == val: + return Number(i + 1) + return Boolean(False) + + +@ns.declare +def zip_(*lists): + return List( + [List(zipped) for zipped in zip(*lists)], + use_comma=True) + + +# TODO need a way to use "list" as the arg name without shadowing the builtin +@ns.declare +def list_separator(list): + if list.use_comma: + return String.unquoted('comma') + else: + return String.unquoted('space') + + +# ------------------------------------------------------------------------------ +# Map functions + +@ns.declare +def map_get(map, key): + return map.to_dict().get(key, Null()) + + +@ns.declare +def map_merge(*maps): + key_order = [] + index = {} + for map in maps: + for key, value in map.to_pairs(): + if key not in index: + key_order.append(key) + + index[key] = value + + pairs = [(key, index[key]) for key in key_order] + return Map(pairs, index=index) + + +@ns.declare +def map_keys(map): + return List( + [k for (k, v) in map.to_pairs()], + use_comma=True) + + +@ns.declare +def map_values(map): + return List( + [v for (k, v) in map.to_pairs()], + use_comma=True) + + +@ns.declare +def map_has_key(map, key): + return Boolean(key in map.to_dict()) + + +# DEVIATIONS: these do not exist in ruby sass + +@ns.declare +def map_get3(map, key, default): + return map.to_dict().get(key, default) + + +@ns.declare +def map_get_nested3(map, keys, default=Null()): + for key in keys: + map = map.to_dict().get(key, None) + if map is None: + return default + + return map + + +@ns.declare +def map_merge_deep(*maps): + pairs = [] + keys = set() + for map in maps: + for key, value in map.to_pairs(): + keys.add(key) + + for key in keys: + values = [map.to_dict().get(key, None) for map in maps] + values = [v for v in values if v is not None] + if all(isinstance(v, Map) for v in values): + pairs.append((key, map_merge_deep(*values))) + else: + pairs.append((key, values[-1])) + + return Map(pairs) + + +@ns.declare +def keywords(value): + """Extract named arguments, as a map, from an argument list.""" + expect_type(value, Arglist) + return value.extract_keywords() + + +# ------------------------------------------------------------------------------ +# Introspection functions + +# TODO feature-exists + +@ns.declare_internal +def variable_exists(namespace, name): + expect_type(name, String) + try: + namespace.variable('$' + name.value) + except KeyError: + return Boolean(False) + else: + return Boolean(True) + + +@ns.declare_internal +def global_variable_exists(namespace, name): + expect_type(name, String) + + # TODO this is... imperfect and invasive, but should be a good + # approximation + scope = namespace._variables + while len(scope.maps) > 1: + scope = scope.maps[-1] + + try: + scope['$' + name.value] + except KeyError: + return Boolean(False) + else: + return Boolean(True) + + +@ns.declare_internal +def function_exists(namespace, name): + expect_type(name, String) + # TODO invasive, but there's no other way to ask for this at the moment + for fname, arity in namespace._functions.keys(): + if name.value == fname: + return Boolean(True) + return Boolean(False) + + +@ns.declare_internal +def mixin_exists(namespace, name): + expect_type(name, String) + # TODO invasive, but there's no other way to ask for this at the moment + for fname, arity in namespace._mixins.keys(): + if name.value == fname: + return Boolean(True) + return Boolean(False) + + +@ns.declare +def inspect(value): + return String.unquoted(value.render()) + + +@ns.declare +def type_of(obj): # -> bool, number, string, color, list + return String(obj.sass_type_name) + + +@ns.declare +def unit(number): # -> px, em, cm, etc. + numer = '*'.join(sorted(number.unit_numer)) + denom = '*'.join(sorted(number.unit_denom)) + + if denom: + ret = numer + '/' + denom + else: + ret = numer + return String.unquoted(ret) + + +@ns.declare +def unitless(value): + if not isinstance(value, Number): + raise TypeError("Expected number, got %r" % (value,)) + + return Boolean(value.is_unitless) + + +@ns.declare +def comparable(number1, number2): + left = number1.to_base_units() + right = number2.to_base_units() + return Boolean( + left.unit_numer == right.unit_numer + and left.unit_denom == right.unit_denom) + + +# TODO call diff --git a/aip_site/vendor/pyscss/scss/extension/extra.py b/aip_site/vendor/pyscss/scss/extension/extra.py new file mode 100644 index 0000000..3edc9f3 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/extra.py @@ -0,0 +1,463 @@ +"""Functions new to the pyScss library.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import os.path +import random + +import six +from six.moves import xrange + +from scss import config +from scss.errors import SassMissingDependency +from scss.extension import Extension +from scss.namespace import Namespace +from scss.types import Color, Number, String, List +from scss.util import escape, make_data_url, make_filename_hash + +try: + from PIL import Image, ImageDraw +except ImportError: + try: + import Image + import ImageDraw + except ImportError: + Image = None + ImageDraw = None + + +log = logging.getLogger(__name__) + + +class ExtraExtension(Extension): + """Extra functions unique to the pyScss library.""" + name = 'extra' + namespace = Namespace() + + +# Alias to make the below declarations less noisy +ns = ExtraExtension.namespace + + +# ------------------------------------------------------------------------------ +# Image stuff + +def _image_noise(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, background=None): + if not density: + density = [0.8] + elif not isinstance(density, (tuple, list)): + density = [density] + + if not intensity: + intensity = [0.5] + elif not isinstance(intensity, (tuple, list)): + intensity = [intensity] + + if not color: + color = [(0, 0, 0, 0)] + elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)): + color = [color] + + if not opacity: + opacity = [0.2] + elif not isinstance(opacity, (tuple, list)): + opacity = [opacity] + + if not monochrome: + monochrome = [False] + elif not isinstance(monochrome, (tuple, list)): + monochrome = [monochrome] + + pixels = {} + + if background: + for y in xrange(size): + for x in xrange(size): + ca = float(background[3]) + pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca) + + loops = max(map(len, (density, intensity, color, opacity, monochrome))) + for l in range(loops): + _density = density[l % len(density)] + _intensity = intensity[l % len(intensity)] + _color = color[l % len(color)] + _opacity = opacity[l % len(opacity)] + _monochrome = monochrome[l % len(monochrome)] + _intensity = 1 - _intensity + if _intensity < 0.5: + cx = 255 * _intensity + cm = cx + else: + cx = 255 * (1 - _intensity) + cm = 255 * _intensity + xa = int(cm - cx) + xb = int(cm + cx) + if xa > 0: + xa &= 255 + else: + xa = 0 + if xb > 0: + xb &= 255 + else: + xb = 0 + r, g, b, a = _color + for i in xrange(int(round(_density * size ** 2))): + x = random.randint(1, size) + y = random.randint(1, size) + cc = random.randint(xa, xb) + cr = (cc) * (1 - a) + a * r + cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g + cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b + ca = random.random() * _opacity + ica = 1 - ca + pos = (x - 1, y - 1) + dst = pixels.get(pos, (0, 0, 0, 0)) + src = (cr * ca, cg * ca, cb * ca, ca) + pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica) + + for pos, col in pixels.items(): + ca = col[3] + if ca: + pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255)) + + +def _image_brushed(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, direction=None, spread=None, background=None): + if not density: + density = [0.8] + elif not isinstance(density, (tuple, list)): + density = [density] + + if not intensity: + intensity = [0.5] + elif not isinstance(intensity, (tuple, list)): + intensity = [intensity] + + if not color: + color = [(0, 0, 0, 0)] + elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)): + color = [color] + + if not opacity: + opacity = [0.2] + elif not isinstance(opacity, (tuple, list)): + opacity = [opacity] + + if not monochrome: + monochrome = [False] + elif not isinstance(monochrome, (tuple, list)): + monochrome = [monochrome] + + if not direction: + direction = [0] + elif not isinstance(direction, (tuple, list)): + direction = [direction] + + if not spread: + spread = [0] + elif not isinstance(spread, (tuple, list)): + spread = [spread] + + def ppgen(d): + if d is None: + return + d = d % 4 + if d == 0: + pp = lambda x, y, o: ((x - o) % size, y) + elif d == 1: + pp = lambda x, y, o: ((x - o) % size, (y + x - o) % size) + elif d == 2: + pp = lambda x, y, o: (y, (x - o) % size) + else: + pp = lambda x, y, o: ((x - o) % size, (y - x - o) % size) + return pp + + pixels = {} + + if background: + for y in xrange(size): + for x in xrange(size): + ca = float(background[3]) + pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca) + + loops = max(map(len, (density, intensity, color, opacity, monochrome, direction, spread))) + for l in range(loops): + _density = density[l % len(density)] + _intensity = intensity[l % len(intensity)] + _color = color[l % len(color)] + _opacity = opacity[l % len(opacity)] + _monochrome = monochrome[l % len(monochrome)] + _direction = direction[l % len(direction)] + _spread = spread[l % len(spread)] + _intensity = 1 - _intensity + if _intensity < 0.5: + cx = 255 * _intensity + cm = cx + else: + cx = 255 * (1 - _intensity) + cm = 255 * _intensity + xa = int(cm - cx) + xb = int(cm + cx) + if xa > 0: + xa &= 255 + else: + xa = 0 + if xb > 0: + xb &= 255 + else: + xb = 0 + r, g, b, a = _color + pp = ppgen(_direction) + if pp: + for y in xrange(size): + if _spread and (y + (l % 2)) % _spread: + continue + o = random.randint(1, size) + cc = random.randint(xa, xb) + cr = (cc) * (1 - a) + a * r + cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g + cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b + da = random.randint(0, 255) * _opacity + ip = round((size / 2.0 * _density) / int(1 / _density)) + iq = round((size / 2.0 * (1 - _density)) / int(1 / _density)) + if ip: + i = da / ip + aa = 0 + else: + i = 0 + aa = da + d = 0 + p = ip + for x in xrange(size): + if d == 0: + if p > 0: + p -= 1 + aa += i + else: + d = 1 + q = iq + elif d == 1: + if q > 0: + q -= 1 + else: + d = 2 + p = ip + elif d == 2: + if p > 0: + p -= 1 + aa -= i + else: + d = 3 + q = iq + elif d == 3: + if q > 0: + q -= 1 + else: + d = 0 + p = ip + if aa > 0: + ca = aa / 255.0 + else: + ca = 0.0 + ica = 1 - ca + pos = pp(x, y, o) + dst = pixels.get(pos, (0, 0, 0, 0)) + src = (cr * ca, cg * ca, cb * ca, ca) + pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica) + + for pos, col in pixels.items(): + ca = col[3] + if ca: + pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255)) + + +@ns.declare +def background_noise(density=None, opacity=None, size=None, monochrome=False, intensity=(), color=None, background=None, inline=False): + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + + density = [Number(v).value for v in List.from_maybe(density)] + intensity = [Number(v).value for v in List.from_maybe(intensity)] + color = [Color(v).value for v in List.from_maybe(color) if v] + opacity = [Number(v).value for v in List.from_maybe(opacity)] + + size = int(Number(size).value) if size else 0 + if size < 1 or size > 512: + size = 200 + + monochrome = bool(monochrome) + + background = Color(background).value if background else None + + new_image = Image.new( + mode='RGBA', + size=(size, size) + ) + + pixdata = new_image.load() + _image_noise(pixdata, size, density, intensity, color, opacity, monochrome) + + if not inline: + key = (size, density, intensity, color, opacity, monochrome) + asset_file = 'noise-%s%sx%s' % ('mono-' if monochrome else '', size, size) + # asset_file += '-[%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or [])) + asset_file += '-' + make_filename_hash(key) + asset_file += '.png' + asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file) + try: + new_image.save(asset_path) + except IOError: + log.exception("Error while saving image") + inline = True # Retry inline version + url = '%s%s' % (config.ASSETS_URL, asset_file) + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url('image/png', contents) + + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +@ns.declare +def background_brushed(density=None, intensity=None, color=None, opacity=None, size=None, monochrome=False, direction=(), spread=(), background=None, inline=False): + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + + density = [Number(v).value for v in List.from_maybe(density)] + intensity = [Number(v).value for v in List.from_maybe(intensity)] + color = [Color(v).value for v in List.from_maybe(color) if v] + opacity = [Number(v).value for v in List.from_maybe(opacity)] + + size = int(Number(size).value) if size else -1 + if size < 0 or size > 512: + size = 200 + + monochrome = bool(monochrome) + + direction = [Number(v).value for v in List.from_maybe(direction)] + spread = [Number(v).value for v in List.from_maybe(spread)] + + background = Color(background).value if background else None + + new_image = Image.new( + mode='RGBA', + size=(size, size) + ) + + pixdata = new_image.load() + _image_brushed(pixdata, size, density, intensity, color, opacity, monochrome, direction, spread, background) + + if not inline: + key = (size, density, intensity, color, opacity, monochrome, direction, spread, background) + asset_file = 'brushed-%s%sx%s' % ('mono-' if monochrome else '', size, size) + # asset_file += '-[%s][%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []), '-'.join(to_str(s).replace('.', '_') for s in direction or [])) + asset_file += '-' + make_filename_hash(key) + asset_file += '.png' + asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file) + try: + new_image.save(asset_path) + except IOError: + log.exception("Error while saving image") + inline = True # Retry inline version + url = '%s%s' % (config.ASSETS_URL, asset_file) + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url('image/png', contents) + + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +@ns.declare +def grid_image(left_gutter, width, right_gutter, height, columns=1, grid_color=None, baseline_color=None, background_color=None, inline=False): + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + if grid_color is None: + grid_color = (120, 170, 250, 15) + else: + c = Color(grid_color).value + grid_color = (c[0], c[1], c[2], int(c[3] * 255.0)) + if baseline_color is None: + baseline_color = (120, 170, 250, 30) + else: + c = Color(baseline_color).value + baseline_color = (c[0], c[1], c[2], int(c[3] * 255.0)) + if background_color is None: + background_color = (0, 0, 0, 0) + else: + c = Color(background_color).value + background_color = (c[0], c[1], c[2], int(c[3] * 255.0)) + _height = int(height) if height >= 1 else int(height * 1000.0) + _width = int(width) if width >= 1 else int(width * 1000.0) + _left_gutter = int(left_gutter) if left_gutter >= 1 else int(left_gutter * 1000.0) + _right_gutter = int(right_gutter) if right_gutter >= 1 else int(right_gutter * 1000.0) + if _height <= 0 or _width <= 0 or _left_gutter <= 0 or _right_gutter <= 0: + raise ValueError + _full_width = (_left_gutter + _width + _right_gutter) + new_image = Image.new( + mode='RGBA', + size=(_full_width * int(columns), _height), + color=background_color + ) + draw = ImageDraw.Draw(new_image) + for i in range(int(columns)): + draw.rectangle((i * _full_width + _left_gutter, 0, i * _full_width + _left_gutter + _width - 1, _height - 1), fill=grid_color) + if _height > 1: + draw.rectangle((0, _height - 1, _full_width * int(columns) - 1, _height - 1), fill=baseline_color) + if not inline: + grid_name = 'grid_' + if left_gutter: + grid_name += str(int(left_gutter)) + '+' + grid_name += str(int(width)) + if right_gutter: + grid_name += '+' + str(int(right_gutter)) + if height and height > 1: + grid_name += 'x' + str(int(height)) + key = (columns, grid_color, baseline_color, background_color) + key = grid_name + '-' + make_filename_hash(key) + asset_file = key + '.png' + asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file) + try: + new_image.save(asset_path) + except IOError: + log.exception("Error while saving image") + inline = True # Retry inline version + url = '%s%s' % (config.ASSETS_URL, asset_file) + if inline: + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url('image/png', contents) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) + + +@ns.declare +def image_color(color, width=1, height=1): + if not Image: + raise SassMissingDependency('PIL', 'image manipulation') + w = int(Number(width).value) + h = int(Number(height).value) + if w <= 0 or h <= 0: + raise ValueError + new_image = Image.new( + mode='RGB' if color.alpha == 1 else 'RGBA', + size=(w, h), + color=color.rgba255, + ) + output = six.BytesIO() + new_image.save(output, format='PNG') + contents = output.getvalue() + output.close() + url = make_data_url('image/png', contents) + inline = 'url("%s")' % escape(url) + return String.unquoted(inline) diff --git a/aip_site/vendor/pyscss/scss/extension/fonts.py b/aip_site/vendor/pyscss/scss/extension/fonts.py new file mode 100644 index 0000000..bc06e42 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/extension/fonts.py @@ -0,0 +1,460 @@ +"""Functions used for generating custom fonts from SVG files.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import re +import errno +import glob +import logging +import os +import time +import tempfile +import subprocess +import warnings +import six + +try: + import cPickle as pickle +except ImportError: + import pickle + +try: + import fontforge +except: + fontforge = None + +from scss import config +from scss.errors import SassMissingDependency +from scss.extension import Extension +from scss.namespace import Namespace +from scss.types import Boolean, List, String, Url +from scss.util import getmtime, make_data_url, make_filename_hash +from scss.extension import Cache + +log = logging.getLogger(__name__) + +TTFAUTOHINT_EXECUTABLE = 'ttfautohint' +TTF2EOT_EXECUTABLE = 'ttf2eot' + +MAX_FONT_SHEETS = 4096 +KEEP_FONT_SHEETS = int(MAX_FONT_SHEETS * 0.8) + +FONT_TYPES = ('eot', 'woff', 'ttf', 'svg') # eot should be first for IE support + +FONT_MIME_TYPES = { + 'ttf': 'application/x-font-ttf', + 'svg': 'image/svg+xml', + 'woff': 'application/x-font-woff', + 'eot': 'application/vnd.ms-fontobject', +} + +FONT_FORMATS = { + 'ttf': "format('truetype')", + 'svg': "format('svg')", + 'woff': "format('woff')", + 'eot': "format('embedded-opentype')", +} + +GLYPH_WIDTH_RE = re.compile(r'width="(\d+(\.\d+)?)') +GLYPH_HEIGHT_RE = re.compile(r'height="(\d+(\.\d+)?)') + +GLYPH_HEIGHT = 512 +GLYPH_ASCENT = 448 +GLYPH_DESCENT = GLYPH_HEIGHT - GLYPH_ASCENT +GLYPH_WIDTH = GLYPH_HEIGHT + +# Offset to work around Chrome Windows bug +GLYPH_START = 0xf100 + + +class FontsExtension(Extension): + """Functions for creating and manipulating fonts.""" + name = 'fonts' + namespace = Namespace() + + +# Alias to make the below declarations less noisy +ns = FontsExtension.namespace + + +def _assets_root(): + return config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets') + + +def _get_cache(prefix): + return Cache((config.CACHE_ROOT or _assets_root(), prefix)) + + +def ttfautohint(ttf): + try: + proc = subprocess.Popen( + [TTFAUTOHINT_EXECUTABLE, '--hinting-limit=200', '--hinting-range-max=50', '--symbol'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + except OSError as e: + if e.errno in (errno.EACCES, errno.ENOENT): + warnings.warn('Could not autohint ttf font: The executable %s could not be run: %s' % (TTFAUTOHINT_EXECUTABLE, e)) + return None + else: + raise e + output, output_err = proc.communicate(ttf) + if proc.returncode != 0: + warnings.warn("Could not autohint ttf font: Unknown error!") + return None + return output + + +def ttf2eot(ttf): + try: + proc = subprocess.Popen( + [TTF2EOT_EXECUTABLE], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + except OSError as e: + if e.errno in (errno.EACCES, errno.ENOENT): + warnings.warn('Could not generate eot font: The executable %s could not be run: %s' % (TTF2EOT_EXECUTABLE, e)) + return None + else: + raise e + output, output_err = proc.communicate(ttf) + if proc.returncode != 0: + warnings.warn("Could not generate eot font: Unknown error!") + return None + return output + + +@ns.declare +def font_sheet(g, **kwargs): + if not fontforge: + raise SassMissingDependency('fontforge', 'font manipulation') + + font_sheets = _get_cache('font_sheets') + + now_time = time.time() + + globs = String(g, quotes=None).value + globs = sorted(g.strip() for g in globs.split(',')) + + _k_ = ','.join(globs) + + files = None + rfiles = None + tfiles = None + base_name = None + glob_path = None + glyph_name = None + + if _k_ in font_sheets: + font_sheets[_k_]['*'] = now_time + else: + files = [] + rfiles = [] + tfiles = [] + for _glob in globs: + if '..' not in _glob: # Protect against going to prohibited places... + if callable(config.STATIC_ROOT): + _glob_path = _glob + _rfiles = _files = sorted(config.STATIC_ROOT(_glob)) + else: + _glob_path = os.path.join(config.STATIC_ROOT, _glob) + _files = glob.glob(_glob_path) + _files = sorted((f, None) for f in _files) + _rfiles = [(rf[len(config.STATIC_ROOT):], s) for rf, s in _files] + if _files: + files.extend(_files) + rfiles.extend(_rfiles) + base_name = os.path.basename(os.path.dirname(_glob)) + _glyph_name, _, _glyph_type = base_name.partition('.') + if _glyph_type: + _glyph_type += '-' + if not glyph_name: + glyph_name = _glyph_name + tfiles.extend([_glyph_type] * len(_files)) + else: + glob_path = _glob_path + + if files is not None: + if not files: + log.error("Nothing found at '%s'", glob_path) + return String.unquoted('') + + key = [f for (f, s) in files] + [repr(kwargs), config.ASSETS_URL] + key = glyph_name + '-' + make_filename_hash(key) + asset_files = { + 'eot': key + '.eot', + 'woff': key + '.woff', + 'ttf': key + '.ttf', + 'svg': key + '.svg', + } + ASSETS_ROOT = _assets_root() + asset_paths = dict((type_, os.path.join(ASSETS_ROOT, asset_file)) for type_, asset_file in asset_files.items()) + cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, key + '.cache') + + inline = Boolean(kwargs.get('inline', False)) + + font_sheet = None + asset = None + file_assets = {} + inline_assets = {} + if all(os.path.exists(asset_path) for asset_path in asset_paths.values()) or inline: + try: + save_time, file_assets, inline_assets, font_sheet, codepoints = pickle.load(open(cache_path)) + if file_assets: + file_asset = List([file_asset for file_asset in file_assets.values()], separator=",") + font_sheets[file_asset.render()] = font_sheet + if inline_assets: + inline_asset = List([inline_asset for inline_asset in inline_assets.values()], separator=",") + font_sheets[inline_asset.render()] = font_sheet + if inline: + asset = inline_asset + else: + asset = file_asset + except: + pass + + if font_sheet: + for file_, storage in files: + _time = getmtime(file_, storage) + if save_time < _time: + if _time > now_time: + log.warning("File '%s' has a date in the future (cache ignored)" % file_) + font_sheet = None # Invalidate cached custom font + break + + if font_sheet is None or asset is None: + cache_buster = Boolean(kwargs.get('cache_buster', True)) + autowidth = Boolean(kwargs.get('autowidth', False)) + autohint = Boolean(kwargs.get('autohint', True)) + + font = fontforge.font() + font.encoding = 'UnicodeFull' + font.design_size = 16 + font.em = GLYPH_HEIGHT + font.ascent = GLYPH_ASCENT + font.descent = GLYPH_DESCENT + font.fontname = glyph_name + font.familyname = glyph_name + font.fullname = glyph_name + + def glyphs(f=lambda x: x): + for file_, storage in f(files): + if storage is not None: + _file = storage.open(file_) + else: + _file = open(file_) + svgtext = _file.read() + svgtext = svgtext.replace('', '') + svgtext = svgtext.replace('', '') + svgtext = svgtext.replace('', '') + m = GLYPH_WIDTH_RE.search(svgtext) + if m: + width = float(m.group(1)) + else: + width = None + m = GLYPH_HEIGHT_RE.search(svgtext) + if m: + height = float(m.group(1)) + else: + height = None + _glyph = tempfile.NamedTemporaryFile(delete=False, suffix=".svg", mode='w') + _glyph.file.write(svgtext) + _glyph.file.close() + yield _glyph.name, width, height + + names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files) + tnames = tuple(tfiles[i] + n for i, n in enumerate(names)) + + codepoints = [] + for i, (glyph_filename, glyph_width, glyph_height) in enumerate(glyphs()): + if glyph_height and glyph_height != GLYPH_HEIGHT: + warnings.warn("Glyphs should be %spx-high" % GLYPH_HEIGHT) + codepoint = i + GLYPH_START + name = names[i] + codepoints.append(codepoint) + glyph = font.createChar(codepoint, name) + glyph.importOutlines(glyph_filename) + os.unlink(glyph_filename) + glyph.width = glyph_width or GLYPH_WIDTH + if autowidth: + # Autowidth removes side bearings + glyph.left_side_bearing = glyph.right_side_bearing = 0 + glyph.round() + + filetime = int(now_time) + + # Generate font files + if not inline: + urls = {} + for type_ in reversed(FONT_TYPES): + asset_path = asset_paths[type_] + try: + if type_ == 'eot': + ttf_path = asset_paths['ttf'] + with open(ttf_path, 'rb') as ttf_fh: + contents = ttf2eot(ttf_fh.read()) + if contents is not None: + with open(asset_path, 'wb') as asset_fh: + asset_fh.write(contents) + else: + font.generate(asset_path) + if type_ == 'ttf': + contents = None + if autohint: + with open(asset_path, 'rb') as asset_fh: + contents = ttfautohint(asset_fh.read()) + if contents is not None: + with open(asset_path, 'wb') as asset_fh: + asset_fh.write(contents) + asset_file = asset_files[type_] + url = '%s%s' % (config.ASSETS_URL, asset_file) + params = [] + if not urls: + params.append('#iefix') + if cache_buster: + params.append('v=%s' % filetime) + if type_ == 'svg': + params.append('#' + glyph_name) + if params: + url += '?' + '&'.join(params) + urls[type_] = url + except IOError: + inline = False + + if inline: + urls = {} + for type_ in reversed(FONT_TYPES): + contents = None + if type_ == 'eot': + ttf_path = asset_paths['ttf'] + with open(ttf_path, 'rb') as ttf_fh: + contents = ttf2eot(ttf_fh.read()) + if contents is None: + continue + else: + _tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.' + type_) + _tmp.file.close() + font.generate(_tmp.name) + with open(_tmp.name, 'rb') as asset_fh: + if autohint: + if type_ == 'ttf': + _contents = asset_fh.read() + contents = ttfautohint(_contents) + if contents is None: + contents = _contents + os.unlink(_tmp.name) + mime_type = FONT_MIME_TYPES[type_] + url = make_data_url(mime_type, contents) + urls[type_] = url + + assets = {} + for type_, url in urls.items(): + format_ = FONT_FORMATS[type_] + if inline: + assets[type_] = inline_assets[type_] = List([Url.unquoted(url), String.unquoted(format_)]) + else: + assets[type_] = file_assets[type_] = List([Url.unquoted(url), String.unquoted(format_)]) + asset = List([assets[type_] for type_ in FONT_TYPES if type_ in assets], separator=",") + + # Add the new object: + font_sheet = dict(zip(tnames, zip(rfiles, codepoints))) + font_sheet['*'] = now_time + font_sheet['*f*'] = asset_files + font_sheet['*k*'] = key + font_sheet['*n*'] = glyph_name + font_sheet['*t*'] = filetime + + codepoints = zip(files, codepoints) + cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT) + pickle.dump((now_time, file_assets, inline_assets, font_sheet, codepoints), cache_tmp) + cache_tmp.close() + os.rename(cache_tmp.name, cache_path) + + # Use the sorted list to remove older elements (keep only 500 objects): + if len(font_sheets) > MAX_FONT_SHEETS: + for a in sorted(font_sheets, key=lambda a: font_sheets[a]['*'], reverse=True)[KEEP_FONT_SHEETS:]: + del font_sheets[a] + log.warning("Exceeded maximum number of font sheets (%s)" % MAX_FONT_SHEETS) + font_sheets[asset.render()] = font_sheet + font_sheet_cache = _get_cache('font_sheet_cache') + for file_, codepoint in codepoints: + font_sheet_cache[file_] = codepoint + # TODO this sometimes returns an empty list, or is never assigned to + return asset + + +@ns.declare_alias('glyph-names') +@ns.declare +def glyphs(sheet, remove_suffix=False): + sheet = sheet.render() + font_sheets = _get_cache('font_sheets') + font_sheet = font_sheets.get(sheet, {}) + return List([String.unquoted(f) for f in sorted(set(f.rsplit('-', 1)[0] if remove_suffix else f for f in font_sheet if not f.startswith('*')))]) + + +@ns.declare +def glyph_classes(sheet): + return glyphs(sheet, True) + + +@ns.declare +def font_url(sheet, type_, only_path=False, cache_buster=True): + font_sheets = _get_cache('font_sheets') + font_sheet = font_sheets.get(sheet.render()) + type_ = String.unquoted(type_).render() + if font_sheet: + asset_files = font_sheet['*f*'] + asset_file = asset_files.get(type_) + if asset_file: + url = '%s%s' % (config.ASSETS_URL, asset_file) + params = [] + # if type_ == 'eot': + # params.append('#iefix') + if cache_buster: + params.append('v=%s' % font_sheet['*t*']) + if type_ == 'svg': + params.append('#' + font_sheet['*n*']) + if params: + url += '?' + '&'.join(params) + if only_path: + return String.unquoted(url) + else: + return Url.unquoted(url) + return String.unquoted('') + + +@ns.declare +def font_format(type_): + type_ = type_.render() + if type_ in FONT_FORMATS: + return String.unquoted(FONT_FORMATS[type_]) + return String.unquoted('') + + +@ns.declare +def has_glyph(sheet, glyph): + sheet = sheet.render() + font_sheets = _get_cache('font_sheets') + font_sheet = font_sheets.get(sheet) + glyph_name = String.unquoted(glyph).value + glyph = font_sheet and font_sheet.get(glyph_name) + if not font_sheet: + log.error("No font sheet found: %s", sheet, extra={'stack': True}) + return Boolean(bool(glyph)) + + +@ns.declare +def glyph_code(sheet, glyph): + sheet = sheet.render() + font_sheets = _get_cache('font_sheets') + font_sheet = font_sheets.get(sheet) + glyph_name = String.unquoted(glyph).value + glyph = font_sheet and font_sheet.get(glyph_name) + if not font_sheet: + log.error("No font sheet found: %s", sheet, extra={'stack': True}) + elif not glyph: + log.error("No glyph found: %s in %s", glyph_name, font_sheet['*n*'], extra={'stack': True}) + return String(six.unichr(glyph[1])) diff --git a/aip_site/vendor/pyscss/scss/grammar/__init__.py b/aip_site/vendor/pyscss/scss/grammar/__init__.py new file mode 100644 index 0000000..ffd5ca8 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/grammar/__init__.py @@ -0,0 +1,9 @@ +"""Grammar and parser plumbing for Sass. Much of this is generated or compiled +in some fashion. +""" +from .scanner import NoMoreTokens +from .scanner import Parser +from .scanner import Scanner +from .scanner import locate_blocks + +__all__ = ('NoMoreTokens', 'Parser', 'Scanner', 'locate_blocks') diff --git a/aip_site/vendor/pyscss/scss/grammar/expression.py b/aip_site/vendor/pyscss/scss/grammar/expression.py new file mode 100644 index 0000000..7dd5ce7 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/grammar/expression.py @@ -0,0 +1,593 @@ +"""Grammar for parsing Sass expressions.""" +# This is a GENERATED FILE -- DO NOT EDIT DIRECTLY! +# Edit scss/grammar/expression.g, then run: +# +# python2 yapps2.py scss/grammar/expression.g +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import operator +import re + +from scss.ast import Parentheses +from scss.ast import UnaryOp +from scss.ast import BinaryOp +from scss.ast import AnyOp +from scss.ast import AllOp +from scss.ast import NotOp +from scss.ast import CallOp +from scss.ast import Interpolation +from scss.ast import Literal +from scss.ast import Variable +from scss.ast import ListLiteral +from scss.ast import MapLiteral +from scss.ast import ArgspecLiteral +from scss.ast import FunctionLiteral +from scss.ast import AlphaFunctionLiteral +from scss.ast import TernaryOp +from scss.cssdefs import unescape +from scss.types import Color +from scss.types import Function +from scss.types import Number +from scss.types import String +from scss.types import Url + +from scss.grammar import Parser +from scss.grammar import Scanner + + + +class SassExpressionScanner(Scanner): + patterns = None + _patterns = [ + ('"="', '='), + ('":"', ':'), + ('","', ','), + ('SINGLE_STRING_GUTS', "([^'\\\\#]|[\\\\].|#(?![{]))*"), + ('DOUBLE_STRING_GUTS', '([^"\\\\#]|[\\\\].|#(?![{]))*'), + ('INTERP_ANYTHING', '([^#]|#(?![{]))*'), + ('INTERP_NO_VARS', '([^#$]|#(?![{]))*'), + ('INTERP_NO_PARENS', '([^#()]|#(?![{]))*'), + ('INTERP_START_URL_HACK', '(?=[#][{])'), + ('INTERP_START', '#[{]'), + ('SPACE', '[ \r\t\n]+'), + ('[ \r\t\n]+', '[ \r\t\n]+'), + ('LPAR', '\\(|\\['), + ('RPAR', '\\)|\\]'), + ('END', '$'), + ('MUL', '[*]'), + ('DIV', '/'), + ('MOD', '(?<=\\s)%'), + ('ADD', '[+]'), + ('SUB', '-\\s'), + ('SIGN', '-(?![a-zA-Z_])'), + ('AND', '(?='), + ('LT', '<'), + ('GT', '>'), + ('DOTDOTDOT', '[.]{3}'), + ('SINGLE_QUOTE', "'"), + ('DOUBLE_QUOTE', '"'), + ('BAREURL_HEAD_HACK', '((?:[\\\\].|[^#$\'"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+)(?=#[{]|\\s*[)])'), + ('BAREURL', '(?:[\\\\].|[^#$\'"()\\x00-\\x08\\x0b\\x0e-\\x20\\x7f]|#(?![{]))+'), + ('UNITS', '(? 1 else v[0] + + def expr_slst(self): + or_expr = self.or_expr() + v = [or_expr] + while self._peek(self.expr_slst_rsts) not in self.expr_slst_chks: + or_expr = self.or_expr() + v.append(or_expr) + return ListLiteral(v, comma=False) if len(v) > 1 else v[0] + + def or_expr(self): + and_expr = self.and_expr() + v = and_expr + while self._peek(self.or_expr_rsts) == 'OR': + OR = self._scan('OR') + and_expr = self.and_expr() + v = AnyOp(v, and_expr) + return v + + def and_expr(self): + not_expr = self.not_expr() + v = not_expr + while self._peek(self.and_expr_rsts) == 'AND': + AND = self._scan('AND') + not_expr = self.not_expr() + v = AllOp(v, not_expr) + return v + + def not_expr(self): + _token_ = self._peek(self.argspec_item_chks) + if _token_ != 'NOT': + comparison = self.comparison() + return comparison + else: # == 'NOT' + NOT = self._scan('NOT') + not_expr = self.not_expr() + return NotOp(not_expr) + + def comparison(self): + a_expr = self.a_expr() + v = a_expr + while self._peek(self.comparison_rsts) in self.comparison_chks: + _token_ = self._peek(self.comparison_chks) + if _token_ == 'LT': + LT = self._scan('LT') + a_expr = self.a_expr() + v = BinaryOp(operator.lt, v, a_expr) + elif _token_ == 'GT': + GT = self._scan('GT') + a_expr = self.a_expr() + v = BinaryOp(operator.gt, v, a_expr) + elif _token_ == 'LE': + LE = self._scan('LE') + a_expr = self.a_expr() + v = BinaryOp(operator.le, v, a_expr) + elif _token_ == 'GE': + GE = self._scan('GE') + a_expr = self.a_expr() + v = BinaryOp(operator.ge, v, a_expr) + elif _token_ == 'EQ': + EQ = self._scan('EQ') + a_expr = self.a_expr() + v = BinaryOp(operator.eq, v, a_expr) + else: # == 'NE' + NE = self._scan('NE') + a_expr = self.a_expr() + v = BinaryOp(operator.ne, v, a_expr) + return v + + def a_expr(self): + m_expr = self.m_expr() + v = m_expr + while self._peek(self.a_expr_rsts) in self.a_expr_chks: + _token_ = self._peek(self.a_expr_chks) + if _token_ == 'ADD': + ADD = self._scan('ADD') + m_expr = self.m_expr() + v = BinaryOp(operator.add, v, m_expr) + else: # == 'SUB' + SUB = self._scan('SUB') + m_expr = self.m_expr() + v = BinaryOp(operator.sub, v, m_expr) + return v + + def m_expr(self): + u_expr = self.u_expr() + v = u_expr + while self._peek(self.m_expr_rsts) in self.m_expr_chks: + _token_ = self._peek(self.m_expr_chks) + if _token_ == 'MUL': + MUL = self._scan('MUL') + u_expr = self.u_expr() + v = BinaryOp(operator.mul, v, u_expr) + elif _token_ == 'DIV': + DIV = self._scan('DIV') + u_expr = self.u_expr() + v = BinaryOp(operator.truediv, v, u_expr) + else: # == 'MOD' + MOD = self._scan('MOD') + u_expr = self.u_expr() + v = BinaryOp(operator.mod, v, u_expr) + return v + + def u_expr(self): + _token_ = self._peek(self.u_expr_rsts) + if _token_ == 'SIGN': + SIGN = self._scan('SIGN') + u_expr = self.u_expr() + return UnaryOp(operator.neg, u_expr) + elif _token_ == 'ADD': + ADD = self._scan('ADD') + u_expr = self.u_expr() + return UnaryOp(operator.pos, u_expr) + else: # in self.u_expr_chks + atom = self.atom() + return atom + + def atom(self): + _token_ = self._peek(self.u_expr_chks) + if _token_ == 'LPAR': + LPAR = self._scan('LPAR') + _token_ = self._peek(self.atom_rsts) + if _token_ == 'RPAR': + v = ListLiteral([], comma=False) + else: # in self.argspec_item_chks + expr_map_or_list = self.expr_map_or_list() + v = expr_map_or_list + RPAR = self._scan('RPAR') + return Parentheses(v) + elif _token_ == 'URL_FUNCTION': + URL_FUNCTION = self._scan('URL_FUNCTION') + LPAR = self._scan('LPAR') + interpolated_url = self.interpolated_url() + RPAR = self._scan('RPAR') + return interpolated_url + elif _token_ == 'ALPHA_FUNCTION': + ALPHA_FUNCTION = self._scan('ALPHA_FUNCTION') + LPAR = self._scan('LPAR') + _token_ = self._peek(self.atom_rsts_) + if _token_ == 'OPACITY': + OPACITY = self._scan('OPACITY') + self._scan('"="') + atom = self.atom() + RPAR = self._scan('RPAR') + return AlphaFunctionLiteral(atom) + else: # in self.atom_chks + argspec = self.argspec() + RPAR = self._scan('RPAR') + return CallOp("alpha", argspec) + elif _token_ == 'IF_FUNCTION': + IF_FUNCTION = self._scan('IF_FUNCTION') + LPAR = self._scan('LPAR') + expr_lst = self.expr_lst() + RPAR = self._scan('RPAR') + return TernaryOp(expr_lst) + elif _token_ == 'LITERAL_FUNCTION': + LITERAL_FUNCTION = self._scan('LITERAL_FUNCTION') + LPAR = self._scan('LPAR') + interpolated_function = self.interpolated_function() + RPAR = self._scan('RPAR') + return Interpolation.maybe(interpolated_function, type=Function, function_name=LITERAL_FUNCTION) + elif _token_ == 'FNCT': + FNCT = self._scan('FNCT') + LPAR = self._scan('LPAR') + argspec = self.argspec() + RPAR = self._scan('RPAR') + return CallOp(FNCT, argspec) + elif _token_ == 'BANG_IMPORTANT': + BANG_IMPORTANT = self._scan('BANG_IMPORTANT') + return Literal(String.unquoted("!important", literal=True)) + elif _token_ in self.atom_chks_: + interpolated_bareword = self.interpolated_bareword() + return Interpolation.maybe(interpolated_bareword) + elif _token_ == 'NUM': + NUM = self._scan('NUM') + UNITS = None + if self._peek(self.atom_rsts__) == 'UNITS': + UNITS = self._scan('UNITS') + return Literal(Number(float(NUM), unit=UNITS)) + elif _token_ not in self.atom_chks__: + interpolated_string = self.interpolated_string() + return interpolated_string + elif _token_ == 'COLOR': + COLOR = self._scan('COLOR') + return Literal(Color.from_hex(COLOR, literal=True)) + else: # == 'VAR' + VAR = self._scan('VAR') + return Variable(VAR) + + def interpolation(self): + INTERP_START = self._scan('INTERP_START') + expr_lst = self.expr_lst() + INTERP_END = self._scan('INTERP_END') + return expr_lst + + def interpolated_url(self): + _token_ = self._peek(self.interpolated_url_rsts) + if _token_ in self.interpolated_url_chks: + interpolated_bare_url = self.interpolated_bare_url() + return Interpolation.maybe(interpolated_bare_url, type=Url, quotes=None) + else: # in self.argspec_item_chks + expr_lst = self.expr_lst() + return FunctionLiteral(expr_lst, "url") + + def interpolated_bare_url(self): + _token_ = self._peek(self.interpolated_url_chks) + if _token_ == 'BAREURL_HEAD_HACK': + BAREURL_HEAD_HACK = self._scan('BAREURL_HEAD_HACK') + parts = [BAREURL_HEAD_HACK] + else: # == 'INTERP_START_URL_HACK' + INTERP_START_URL_HACK = self._scan('INTERP_START_URL_HACK') + parts = [''] + while self._peek(self.interpolated_bare_url_rsts) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + _token_ = self._peek(self.interpolated_bare_url_rsts_) + if _token_ == 'BAREURL': + BAREURL = self._scan('BAREURL') + parts.append(BAREURL) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + else: # in self.interpolated_bare_url_rsts + parts.append('') + return parts + + def interpolated_string(self): + _token_ = self._peek(self.interpolated_string_rsts) + if _token_ == 'SINGLE_QUOTE': + interpolated_string_single = self.interpolated_string_single() + return Interpolation.maybe(interpolated_string_single, quotes="'") + else: # == 'DOUBLE_QUOTE' + interpolated_string_double = self.interpolated_string_double() + return Interpolation.maybe(interpolated_string_double, quotes='"') + + def interpolated_string_single(self): + SINGLE_QUOTE = self._scan('SINGLE_QUOTE') + SINGLE_STRING_GUTS = self._scan('SINGLE_STRING_GUTS') + parts = [unescape(SINGLE_STRING_GUTS)] + while self._peek(self.interpolated_string_single_rsts) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + SINGLE_STRING_GUTS = self._scan('SINGLE_STRING_GUTS') + parts.append(unescape(SINGLE_STRING_GUTS)) + SINGLE_QUOTE = self._scan('SINGLE_QUOTE') + return parts + + def interpolated_string_double(self): + DOUBLE_QUOTE = self._scan('DOUBLE_QUOTE') + DOUBLE_STRING_GUTS = self._scan('DOUBLE_STRING_GUTS') + parts = [unescape(DOUBLE_STRING_GUTS)] + while self._peek(self.interpolated_string_double_rsts) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + DOUBLE_STRING_GUTS = self._scan('DOUBLE_STRING_GUTS') + parts.append(unescape(DOUBLE_STRING_GUTS)) + DOUBLE_QUOTE = self._scan('DOUBLE_QUOTE') + return parts + + def interpolated_bareword(self): + _token_ = self._peek(self.atom_chks_) + if _token_ == 'BAREWORD': + BAREWORD = self._scan('BAREWORD') + parts = [BAREWORD] + if self._peek(self.interpolated_bareword_rsts) == 'SPACE': + SPACE = self._scan('SPACE') + return parts + else: # == 'INTERP_START' + interpolation = self.interpolation() + parts = ['', interpolation] + _token_ = self._peek(self.interpolated_bareword_rsts_) + if _token_ == 'BAREWORD': + BAREWORD = self._scan('BAREWORD') + parts.append(BAREWORD) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + elif 1: + parts.append('') + while self._peek(self.interpolated_bareword_rsts__) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + _token_ = self._peek(self.interpolated_bareword_rsts_) + if _token_ == 'BAREWORD': + BAREWORD = self._scan('BAREWORD') + parts.append(BAREWORD) + elif _token_ == 'SPACE': + SPACE = self._scan('SPACE') + return parts + elif 1: + parts.append('') + return parts + + def interpolated_function(self): + interpolated_function_parens = self.interpolated_function_parens() + parts = interpolated_function_parens + while self._peek(self.interpolated_bare_url_rsts) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + interpolated_function_parens = self.interpolated_function_parens() + parts.extend(interpolated_function_parens) + return parts + + def interpolated_function_parens(self): + INTERP_NO_PARENS = self._scan('INTERP_NO_PARENS') + parts = [INTERP_NO_PARENS] + while self._peek(self.interpolated_function_parens_rsts) == 'LPAR': + LPAR = self._scan('LPAR') + interpolated_function = self.interpolated_function() + parts = parts[:-1] + [parts[-1] + LPAR + interpolated_function[0]] + interpolated_function[1:] + RPAR = self._scan('RPAR') + INTERP_NO_PARENS = self._scan('INTERP_NO_PARENS') + parts[-1] += RPAR + INTERP_NO_PARENS + return parts + + def goal_interpolated_literal(self): + INTERP_ANYTHING = self._scan('INTERP_ANYTHING') + parts = [INTERP_ANYTHING] + while self._peek(self.goal_interpolated_literal_rsts) == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + INTERP_ANYTHING = self._scan('INTERP_ANYTHING') + parts.append(INTERP_ANYTHING) + END = self._scan('END') + return Interpolation.maybe(parts) + + def goal_interpolated_literal_with_vars(self): + INTERP_NO_VARS = self._scan('INTERP_NO_VARS') + parts = [INTERP_NO_VARS] + while self._peek(self.goal_interpolated_literal_with_vars_rsts) != 'END': + _token_ = self._peek(self.goal_interpolated_literal_with_vars_rsts_) + if _token_ == 'INTERP_START': + interpolation = self.interpolation() + parts.append(interpolation) + else: # == 'VAR' + VAR = self._scan('VAR') + parts.append(Variable(VAR)) + INTERP_NO_VARS = self._scan('INTERP_NO_VARS') + parts.append(INTERP_NO_VARS) + END = self._scan('END') + return Interpolation.maybe(parts) + + atom_chks_ = frozenset(['BAREWORD', 'INTERP_START']) + expr_map_or_list_rsts__ = frozenset(['LPAR', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'DOUBLE_QUOTE', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE', '","']) + u_expr_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'VAR', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'SINGLE_QUOTE']) + m_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","']) + interpolated_bare_url_rsts_ = frozenset(['RPAR', 'INTERP_START', 'BAREURL', 'SPACE']) + argspec_items_rsts = frozenset(['RPAR', 'END', '","']) + expr_slst_chks = frozenset(['INTERP_END', 'RPAR', 'END', '":"', '","']) + expr_lst_rsts = frozenset(['INTERP_END', 'RPAR', 'END', '","']) + goal_interpolated_literal_rsts = frozenset(['END', 'INTERP_START']) + expr_map_or_list_rsts = frozenset(['RPAR', '":"', '","']) + goal_interpolated_literal_with_vars_rsts = frozenset(['VAR', 'END', 'INTERP_START']) + argspec_item_chks = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE']) + a_expr_chks = frozenset(['ADD', 'SUB']) + interpolated_function_parens_rsts = frozenset(['LPAR', 'RPAR', 'INTERP_START']) + expr_slst_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'NOT', 'SINGLE_QUOTE', '","']) + interpolated_bareword_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","']) + atom_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'VAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'UNITS', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","']) + or_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OR', 'NOT', 'SINGLE_QUOTE', '","']) + argspec_chks_ = frozenset(['END', 'RPAR']) + interpolated_string_single_rsts = frozenset(['SINGLE_QUOTE', 'INTERP_START']) + interpolated_bareword_rsts_ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'INTERP_END', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'GT', 'END', 'SPACE', 'SIGN', 'LITERAL_FUNCTION', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","']) + and_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', '":"', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'AND', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + comparison_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'ADD', 'FNCT', 'VAR', 'EQ', 'AND', 'GE', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + argspec_chks = frozenset(['DOTDOTDOT', 'SLURPYVAR']) + atom_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'OPACITY', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE']) + interpolated_string_double_rsts = frozenset(['DOUBLE_QUOTE', 'INTERP_START']) + atom_chks__ = frozenset(['COLOR', 'VAR']) + expr_map_or_list_rsts_ = frozenset(['RPAR', '","']) + u_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'SINGLE_QUOTE']) + interpolated_url_chks = frozenset(['INTERP_START_URL_HACK', 'BAREURL_HEAD_HACK']) + atom_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE']) + interpolated_url_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'SINGLE_QUOTE', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'INTERP_START_URL_HACK', 'IF_FUNCTION', 'BAREURL_HEAD_HACK']) + comparison_chks = frozenset(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ']) + argspec_items_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE']) + a_expr_rsts = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'INTERP_END', 'BANG_IMPORTANT', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'OR', '","']) + interpolated_string_rsts = frozenset(['DOUBLE_QUOTE', 'SINGLE_QUOTE']) + interpolated_bareword_rsts__ = frozenset(['LPAR', 'DOUBLE_QUOTE', 'SUB', 'ALPHA_FUNCTION', 'RPAR', 'MUL', 'INTERP_END', 'BANG_IMPORTANT', 'DIV', 'LE', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NE', 'LT', 'NUM', '":"', 'LITERAL_FUNCTION', 'IF_FUNCTION', 'GT', 'END', 'SIGN', 'BAREWORD', 'GE', 'FNCT', 'VAR', 'EQ', 'AND', 'ADD', 'SINGLE_QUOTE', 'NOT', 'MOD', 'OR', '","']) + m_expr_chks = frozenset(['MUL', 'DIV', 'MOD']) + goal_interpolated_literal_with_vars_rsts_ = frozenset(['VAR', 'INTERP_START']) + interpolated_bare_url_rsts = frozenset(['RPAR', 'INTERP_START']) + argspec_items_chks = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE']) + argspec_rsts = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'SLURPYVAR', 'ALPHA_FUNCTION', 'RPAR', 'BANG_IMPORTANT', 'URL_FUNCTION', 'INTERP_START', 'COLOR', 'NUM', 'BAREWORD', 'IF_FUNCTION', 'END', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'FNCT', 'VAR', 'DOTDOTDOT', 'NOT', 'SINGLE_QUOTE']) + atom_rsts = frozenset(['BANG_IMPORTANT', 'LPAR', 'DOUBLE_QUOTE', 'IF_FUNCTION', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE']) + argspec_items_rsts__ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'BANG_IMPORTANT', 'SLURPYVAR', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'DOTDOTDOT', 'INTERP_START', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'IF_FUNCTION', 'SINGLE_QUOTE']) + argspec_rsts_ = frozenset(['KWVAR', 'LPAR', 'DOUBLE_QUOTE', 'IF_FUNCTION', 'END', 'URL_FUNCTION', 'BAREWORD', 'COLOR', 'ALPHA_FUNCTION', 'INTERP_START', 'BANG_IMPORTANT', 'SIGN', 'LITERAL_FUNCTION', 'ADD', 'NUM', 'VAR', 'FNCT', 'NOT', 'RPAR', 'SINGLE_QUOTE']) + + diff --git a/aip_site/vendor/pyscss/scss/grammar/scanner.py b/aip_site/vendor/pyscss/scss/grammar/scanner.py new file mode 100644 index 0000000..aef5b58 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/grammar/scanner.py @@ -0,0 +1,283 @@ +"""Pure-Python scanner and parser, used if the C module is not available.""" +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +from collections import deque +import re + +from scss.errors import SassSyntaxError + + +DEBUG = False + + +try: + from ._scanner import locate_blocks +except ImportError: + # Regex for finding a minimum set of characters that might affect where a + # block starts or ends + _blocks_re = re.compile(r'[{},;()\'"\n]|\\.', re.DOTALL) + + def locate_blocks(codestr): + """ + For processing CSS like strings. + + Either returns all selectors (that can be "smart" multi-lined, as + long as it's joined by `,`, or enclosed in `(` and `)`) with its code block + (the one between `{` and `}`, which can be nested), or the "lose" code + (properties) that doesn't have any blocks. + """ + lineno = 1 + + par = 0 + instr = None + depth = 0 + skip = False + i = init = lose = 0 + start = end = None + lineno_stack = deque() + + for m in _blocks_re.finditer(codestr): + i = m.start(0) + c = codestr[i] + if c == '\n': + lineno += 1 + + if c == '\\': + # Escape, also consumes the next character + pass + elif instr is not None: + if c == instr: + instr = None # A string ends (FIXME: needs to accept escaped characters) + elif c in ('"', "'"): + instr = c # A string starts + elif c == '(': # parenthesis begins: + par += 1 + elif c == ')': # parenthesis ends: + par -= 1 + elif not par and not instr: + if c == '{': # block begins: + if depth == 0: + if i > 0 and codestr[i - 1] == '#': # Do not process #{...} as blocks! + skip = True + else: + lineno_stack.append(lineno) + start = i + if lose < init: + _property = codestr[lose:init].strip() + if _property: + yield lineno, _property, None + lose = init + depth += 1 + elif c == '}': # block ends: + if depth <= 0: + raise SyntaxError("Unexpected closing brace on line {0}".format(lineno)) + else: + depth -= 1 + if depth == 0: + if not skip: + end = i + _selectors = codestr[init:start].strip() + _codestr = codestr[start + 1:end].strip() + if _selectors: + yield lineno_stack.pop(), _selectors, _codestr + init = lose = end + 1 + skip = False + elif depth == 0: + if c == ';': # End of property (or block): + init = i + if lose < init: + _property = codestr[lose:init].strip() + if _property: + yield lineno, _property, None + init = lose = i + 1 + if depth > 0: + if not skip: + _selectors = codestr[init:start].strip() + _codestr = codestr[start + 1:].strip() + if _selectors: + yield lineno, _selectors, _codestr + if par: + error = "Parentheses never closed" + elif instr: + error = "String literal never terminated" + else: + error = "Block never closed" + # TODO should remember the line + position of the actual + # problem, and show it in a SassError + raise SyntaxError( + "Couldn't parse block starting on line {0}: {1}" + .format(lineno, error) + ) + losestr = codestr[lose:] + for _property in losestr.split(';'): + _property = _property.strip() + lineno += _property.count('\n') + if _property: + yield lineno, _property, None + + +################################################################################ +# Parser + +# NOTE: This class has no C equivalent +class Parser(object): + def __init__(self, scanner): + self._scanner = scanner + self._pos = 0 + self._char_pos = 0 + + def reset(self, input): + self._scanner.reset(input) + self._pos = 0 + self._char_pos = 0 + + def _peek(self, types): + """ + Returns the token type for lookahead; if there are any args + then the list of args is the set of token types to allow + """ + tok = self._scanner.token(self._pos, types) + return tok[2] + + def _scan(self, type): + """ + Returns the matched text, and moves to the next token + """ + tok = self._scanner.token(self._pos, frozenset([type])) + self._char_pos = tok[0] + if tok[2] != type: + raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(tok[0]), "Trying to find " + type)) + self._pos += 1 + return tok[3] + + +try: + from ._scanner import NoMoreTokens +except ImportError: + class NoMoreTokens(Exception): + """ + Another exception object, for when we run out of tokens + """ + pass + + +try: + from ._scanner import Scanner +except ImportError: + class Scanner(object): + def __init__(self, patterns, ignore, input=None): + """ + Patterns is [(terminal,regex)...] + Ignore is [terminal,...]; + Input is a string + """ + self.reset(input) + self.ignore = ignore + # The stored patterns are a pair (compiled regex,source + # regex). If the patterns variable passed in to the + # constructor is None, we assume that the class already has a + # proper .patterns list constructed + if patterns is not None: + self.patterns = [] + for k, r in patterns: + self.patterns.append((k, re.compile(r))) + + def reset(self, input): + self.tokens = [] + self.restrictions = [] + self.input = input + self.pos = 0 + + def __repr__(self): + """ + Print the last 10 tokens that have been scanned in + """ + output = '' + for t in self.tokens[-10:]: + output = "%s\n (@%s) %s = %s" % (output, t[0], t[2], repr(t[3])) + return output + + def _scan(self, restrict): + """ + Should scan another token and add it to the list, self.tokens, + and add the restriction to self.restrictions + """ + # Keep looking for a token, ignoring any in self.ignore + if DEBUG: + print() + print("Being asked to match with restriction:", repr(restrict)) + token = None + while True: + best_pat = None + # Search the patterns for a match, with earlier + # tokens in the list having preference + best_pat_len = 0 + for tok, regex in self.patterns: + if DEBUG: + print("\tTrying %s: %s at pos %d -> %s" % (repr(tok), repr(regex.pattern), self.pos, repr(self.input))) + # First check to see if we're restricting to this token + if restrict and tok not in restrict and tok not in self.ignore: + if DEBUG: + print("\tSkipping %r!" % (tok,)) + continue + m = regex.match(self.input, self.pos) + if m: + # We got a match + best_pat = tok + best_pat_len = len(m.group(0)) + if DEBUG: + print("Match OK! %s: %s at pos %d" % (repr(tok), repr(regex.pattern), self.pos)) + break + + # If we didn't find anything, raise an error + if best_pat is None: + raise SassSyntaxError(self.input, self.pos, restrict) + + # If we found something that isn't to be ignored, return it + if best_pat in self.ignore: + # This token should be ignored... + self.pos += best_pat_len + else: + end_pos = self.pos + best_pat_len + # Create a token with this data + token = ( + self.pos, + end_pos, + best_pat, + self.input[self.pos:end_pos] + ) + break + if token is not None: + self.pos = token[1] + # Only add this token if it's not in the list + # (to prevent looping) + if not self.tokens or token != self.tokens[-1]: + self.tokens.append(token) + self.restrictions.append(restrict) + return 1 + return 0 + + def token(self, i, restrict=None): + """ + Get the i'th token, and if i is one past the end, then scan + for another token; restrict is a list of tokens that + are allowed, or 0 for any token. + """ + tokens_len = len(self.tokens) + if i == tokens_len: # We are at the end, get the next... + tokens_len += self._scan(restrict) + if i < tokens_len: + if restrict and self.restrictions[i] and restrict > self.restrictions[i]: + raise NotImplementedError("Unimplemented: restriction set changed") + return self.tokens[i] + raise NoMoreTokens + + def rewind(self, i): + tokens_len = len(self.tokens) + if i <= tokens_len: + token = self.tokens[i] + self.tokens = self.tokens[:i] + self.restrictions = self.restrictions[:i] + self.pos = token[0] diff --git a/aip_site/vendor/pyscss/scss/legacy.py b/aip_site/vendor/pyscss/scss/legacy.py new file mode 100644 index 0000000..ec4fd72 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/legacy.py @@ -0,0 +1,208 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import os +from pathlib import Path +from collections import namedtuple + +import six + +from scss.calculator import Calculator +from scss.compiler import Compiler +import scss.config as config +from scss.extension.bootstrap import BootstrapExtension +from scss.extension.core import CoreExtension +from scss.extension.compass import CompassExtension +from scss.extension.extra import ExtraExtension +from scss.extension.fonts import FontsExtension +from scss.namespace import Namespace +from scss.scss_meta import ( + BUILD_INFO, PROJECT, VERSION, REVISION, URL, AUTHOR, AUTHOR_EMAIL, LICENSE, +) +from scss.source import SourceFile +from scss.types import String + + +_default_scss_vars = { + '$BUILD-INFO': String.unquoted(BUILD_INFO), + '$PROJECT': String.unquoted(PROJECT), + '$VERSION': String.unquoted(VERSION), + '$REVISION': String.unquoted(REVISION), + '$URL': String.unquoted(URL), + '$AUTHOR': String.unquoted(AUTHOR), + '$AUTHOR-EMAIL': String.unquoted(AUTHOR_EMAIL), + '$LICENSE': String.unquoted(LICENSE), + + # unsafe chars will be hidden as vars + '$--doubleslash': String.unquoted('//'), + '$--bigcopen': String.unquoted('/*'), + '$--bigcclose': String.unquoted('*/'), + '$--doubledot': String.unquoted(':'), + '$--semicolon': String.unquoted(';'), + '$--curlybracketopen': String.unquoted('{'), + '$--curlybracketclosed': String.unquoted('}'), +} + +SourceFileTuple = namedtuple('SourceFileTuple', ('parent_dir', 'filename')) + + +# TODO using this should spew an actual deprecation warning +class Scss(object): + """Original programmatic interface to the compiler. + + This class is now DEPRECATED. See :mod:`scss.compiler` for the + replacement. + """ + def __init__( + self, scss_vars=None, scss_opts=None, scss_files=None, + super_selector='', live_errors=False, + library=None, func_registry=None, + search_paths=None): + + self.super_selector = super_selector + + self._scss_vars = {} + if scss_vars: + calculator = Calculator() + for var_name, value in scss_vars.items(): + if isinstance(value, six.string_types): + scss_value = calculator.evaluate_expression(value) + if scss_value is None: + # TODO warning? + scss_value = String.unquoted(value) + else: + scss_value = value + self._scss_vars[var_name] = scss_value + + self._scss_opts = scss_opts or {} + self._scss_files = scss_files + self._library = func_registry or library + self._search_paths = search_paths + + # If true, swallow compile errors and embed them in the output instead + self.live_errors = live_errors + + def compile( + self, scss_string=None, scss_file=None, source_files=None, + super_selector=None, filename=None, is_sass=None, + line_numbers=True, import_static_css=False): + """Compile Sass to CSS. Returns a single CSS string. + + This method is DEPRECATED; see :mod:`scss.compiler` instead. + """ + # Derive our root namespace + self.scss_vars = _default_scss_vars.copy() + if self._scss_vars is not None: + self.scss_vars.update(self._scss_vars) + + root_namespace = Namespace( + variables=self.scss_vars, + functions=self._library, + ) + + # Figure out search paths. Fall back from provided explicitly to + # defined globally to just searching the current directory + search_paths = ['.'] + if self._search_paths is not None: + assert not isinstance(self._search_paths, six.string_types), \ + "`search_paths` should be an iterable, not a string" + search_paths.extend(self._search_paths) + else: + if config.LOAD_PATHS: + if isinstance(config.LOAD_PATHS, six.string_types): + # Back-compat: allow comma-delimited + search_paths.extend(config.LOAD_PATHS.split(',')) + else: + search_paths.extend(config.LOAD_PATHS) + + search_paths.extend(self._scss_opts.get('load_paths', [])) + + # Normalize a few old styles of options + output_style = self._scss_opts.get('style', config.STYLE) + if output_style is True: + output_style = 'compressed' + elif output_style is False: + output_style = 'legacy' + + fixed_search_path = [] + for path in search_paths: + if isinstance(path, six.string_types): + fixed_search_path.append(Path(path)) + else: + fixed_search_path.append(path) + + # Build the compiler + compiler = Compiler( + namespace=root_namespace, + extensions=[ + CoreExtension, + ExtraExtension, + FontsExtension, + CompassExtension, + BootstrapExtension, + ], + search_path=fixed_search_path, + import_static_css=import_static_css, + live_errors=self.live_errors, + generate_source_map=self._scss_opts.get('debug_info', False), + output_style=output_style, + warn_unused_imports=self._scss_opts.get('warn_unused', False), + ignore_parse_errors=config.DEBUG, + loops_have_own_scopes=config.CONTROL_SCOPING, + undefined_variables_fatal=config.FATAL_UNDEFINED, + super_selector=super_selector or self.super_selector, + ) + # Gonna add the source files manually + compilation = compiler.make_compilation() + + # Inject the files we know about + # TODO how does this work with the expectation of absoluteness + if source_files is not None: + for source in source_files: + compilation.add_source(source) + elif scss_string is not None: + source = SourceFile.from_string( + scss_string, + relpath=filename, + is_sass=is_sass, + ) + compilation.add_source(source) + elif scss_file is not None: + # This is now the only way to allow forcibly overriding the + # filename a source "thinks" it is + with open(scss_file, 'rb') as f: + source = SourceFile.from_file( + f, + relpath=filename or scss_file, + is_sass=is_sass, + ) + compilation.add_source(source) + + # Plus the ones from the constructor + if self._scss_files: + for name, contents in list(self._scss_files.items()): + source = SourceFile.from_string(contents, relpath=name) + compilation.add_source(source) + + compiled = compiler.call_and_catch_errors(compilation.run) + self.source_files = list(SourceFileTuple(*os.path.split(s.path)) for s in compilation.source_index.values()) + return compiled + + # Old, old alias + Compilation = compile + + def get_scss_constants(self): + scss_vars = self.root_namespace.variables + return dict( + (k, v) for k, v in scss_vars.items() + if k and (not k.startswith('$') or k[1].isupper()) + ) + + def get_scss_vars(self): + scss_vars = self.root_namespace.variables + return dict( + (k, v) for k, v in scss_vars.items() + if k and not (not k.startswith('$') and k[1].isupper()) + ) diff --git a/aip_site/vendor/pyscss/scss/less2scss.py b/aip_site/vendor/pyscss/scss/less2scss.py new file mode 100644 index 0000000..b352348 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/less2scss.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" +Tool for converting Less files to Scss + +Usage: python -m scss.less2scss [file] + +""" +# http://stackoverflow.com/questions/14970224/anyone-know-of-a-good-way-to-convert-from-less-to-sass +from __future__ import unicode_literals, absolute_import, print_function + +import re +import os +import sys + + +class Less2Scss(object): + at_re = re.compile(r'@(?!(media|import|mixin|font-face)(\s|\())') + mixin_re = re.compile(r'\.([\w\-]*)\s*\((.*)\)\s*\{') + include_re = re.compile(r'(\s|^)\.([\w\-]*\(?.*\)?;)') + functions_map = { + 'spin': 'adjust-hue', + } + functions_re = re.compile(r'(%s)\(' % '|'.join(functions_map)) + + def convert(self, content): + content = self.convertVariables(content) + content = self.convertMixins(content) + content = self.includeMixins(content) + content = self.convertFunctions(content) + return content + + def convertVariables(self, content): + # Matches any @ that doesn't have 'media ' or 'import ' after it. + content = self.at_re.sub('$', content) + return content + + def convertMixins(self, content): + content = self.mixin_re.sub('@mixin \1(\2) {', content) + return content + + def includeMixins(self, content): + content = self.mixin_re.sub('\1@include \2', content) + return content + + def convertFunctions(self, content): + content = self.functions_re.sub(lambda m: '%s(' % self.functions_map[m.group(0)], content) + return content + + +def less2scss(options, args): + if not args: + args = ['-'] + + less2scss = Less2Scss() + + for source_path in args: + if source_path == '-': + source = sys.stdin + destiny = sys.stdout + else: + try: + source = open(source_path) + destiny_path, ext = os.path.splitext(source_path) + destiny_path += '.scss' + if not options.force and os.path.exists(destiny_path): + raise IOError("File already exists: %s" % destiny_path) + destiny = open(destiny_path, 'w') + except Exception as e: + error = "%s" % e + if destiny_path in error: + ignoring = "Ignoring" + else: + ignoring = "Ignoring %s" % destiny_path + print("WARNING -- %s. %s" % (ignoring, error), file=sys.stderr) + continue + content = source.read() + content = less2scss.convert(content) + destiny.write(content) + + +def main(): + from optparse import OptionParser, SUPPRESS_HELP + + parser = OptionParser(usage="Usage: %prog [file]", + description="Converts Less files to Scss.", + add_help_option=False) + parser.add_option("-f", "--force", action="store_true", + dest="force", default=False, + help="Forces overwriting output file if it already exists") + parser.add_option("-?", action="help", help=SUPPRESS_HELP) + parser.add_option("-h", "--help", action="help", + help="Show this message and exit") + parser.add_option("-v", "--version", action="store_true", + help="Print version and exit") + + options, args = parser.parse_args() + + if options.version: + from scss.tool import print_version + print_version() + else: + less2scss(options, args) + + +if __name__ == "__main__": + main() diff --git a/aip_site/vendor/pyscss/scss/namespace.py b/aip_site/vendor/pyscss/scss/namespace.py new file mode 100644 index 0000000..dab3aea --- /dev/null +++ b/aip_site/vendor/pyscss/scss/namespace.py @@ -0,0 +1,265 @@ +"""Support for Sass's namespacing rules.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import inspect +import logging + +import six + +from scss.types import Undefined +from scss.types import Value + + +log = logging.getLogger(__name__) + + +def normalize_var(name): + assert isinstance(name, six.string_types) + return name.replace('_', '-') + + +class Scope(object): + """Implements Sass variable scoping. + + Similar to `ChainMap`, except that assigning a new value will replace an + existing value, not mask it. + """ + def __init__(self, maps=()): + maps = list(maps) + self.maps = [dict()] + maps + + def __repr__(self): + return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join(repr(map) for map in self.maps), id(self)) + + def __getitem__(self, key): + for map in self.maps: + if key in map: + return map[key] + + raise KeyError(key) + + def __setitem__(self, key, value): + self.set(key, value) + + def __contains__(self, key): + for map in self.maps: + if key in map: + return True + return False + + def keys(self): + # For mapping interface + keys = set() + for map in self.maps: + keys.update(map.keys()) + return list(keys) + + def set(self, key, value, force_local=False): + if not force_local: + for map in self.maps: + if key in map: + if isinstance(map[key], Undefined): + break + map[key] = value + return + + self.maps[0][key] = value + + def new_child(self): + return type(self)(self.maps) + + +class VariableScope(Scope): + pass + + +class FunctionScope(Scope): + def __repr__(self): + return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join('[%s]' % ', '.join('%s:%s' % (f, n) for f, n in sorted(map.keys())) for map in self.maps), id(self)) + + +class MixinScope(Scope): + def __repr__(self): + return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join('[%s]' % ', '.join('%s:%s' % (f, n) for f, n in sorted(map.keys())) for map in self.maps), id(self)) + + +class ImportScope(Scope): + pass + + +class Namespace(object): + """...""" + _mutable = True + + def __init__(self, variables=None, functions=None, mixins=None, mutable=True): + self._mutable = mutable + + if variables is None: + self._variables = VariableScope() + else: + # TODO parse into sass values once that's a thing, or require them + # all to be + self._variables = VariableScope([variables]) + + if functions is None: + self._functions = FunctionScope() + else: + self._functions = FunctionScope([functions._functions]) + + self._mixins = MixinScope() + + self._imports = ImportScope() + + def _assert_mutable(self): + if not self._mutable: + raise AttributeError("This Namespace instance is immutable") + + @classmethod + def derive_from(cls, *others): + self = cls() + if len(others) == 1: + self._variables = others[0]._variables.new_child() + self._functions = others[0]._functions.new_child() + self._mixins = others[0]._mixins.new_child() + self._imports = others[0]._imports.new_child() + else: + # Note that this will create a 2-dimensional scope where each of + # these scopes is checked first in order. TODO is this right? + self._variables = VariableScope(other._variables for other in others) + self._functions = FunctionScope(other._functions for other in others) + self._mixins = MixinScope(other._mixins for other in others) + self._imports = ImportScope(other._imports for other in others) + return self + + def derive(self): + """Return a new child namespace. All existing variables are still + readable and writeable, but any new variables will only exist within a + new scope. + """ + return type(self).derive_from(self) + + def declare(self, function): + """Insert a Python function into this Namespace, detecting its name and + argument count automatically. + """ + self._auto_register_function(function, function.__name__) + return function + + def declare_alias(self, name): + """Insert a Python function into this Namespace with an + explicitly-given name, but detect its argument count automatically. + """ + def decorator(f): + self._auto_register_function(f, name) + return f + + return decorator + + def declare_internal(self, function): + """Like declare(), but the registered function will also receive the + current namespace as its first argument. Useful for functions that + inspect the state of the compilation, like ``variable-exists()``. + Probably not so useful for anything else. + """ + function._pyscss_needs_namespace = True + self._auto_register_function(function, function.__name__, 1) + return function + + def _auto_register_function(self, function, name, ignore_args=0): + name = name.replace('_', '-').rstrip('-') + try: + argspec = inspect.getfullargspec(function) + varkw = argspec.varkw + except AttributeError: + # In python 2.7, getfulargspec does not exist. + # Let's use getargspec as fallback. + argspec = inspect.getargspec(function) + varkw = argspec.keywords + + if argspec.varargs or varkw: + # Accepts some arbitrary number of arguments + arities = [None] + else: + # Accepts a fixed range of arguments + if argspec.defaults: + num_optional = len(argspec.defaults) + else: + num_optional = 0 + num_args = len(argspec.args) - ignore_args + arities = range(num_args - num_optional, num_args + 1) + + for arity in arities: + self.set_function(name, arity, function) + + @property + def variables(self): + return dict((k, self._variables[k]) for k in self._variables.keys()) + + def variable(self, name, throw=False): + name = normalize_var(name) + return self._variables[name] + + def set_variable(self, name, value, local_only=False): + self._assert_mutable() + name = normalize_var(name) + if not isinstance(value, Value): + raise TypeError("Expected a Sass type, while setting %s got %r" % (name, value,)) + self._variables.set(name, value, force_local=local_only) + + def has_import(self, source): + return source.path in self._imports + + def add_import(self, source, parent_rule): + self._assert_mutable() + self._imports[source.path] = [ + 0, + parent_rule.source_file.path, + parent_rule.file_and_line, + ] + + def use_import(self, import_key): + self._assert_mutable() + if import_key and import_key in self._imports: + imports = self._imports[import_key] + imports[0] += 1 + self.use_import(imports[1]) + + def unused_imports(self): + unused = [] + for import_key in self._imports.keys(): + imports = self._imports[import_key] + if not imports[0]: + unused.append((import_key[0], imports[2])) + return unused + + def _get_callable(self, chainmap, name, arity): + name = normalize_var(name) + if arity is not None: + # With explicit arity, try the particular arity before falling back + # to the general case (None) + try: + return chainmap[name, arity] + except KeyError: + pass + + return chainmap[name, None] + + def _set_callable(self, chainmap, name, arity, cb): + name = normalize_var(name) + chainmap[name, arity] = cb + + def mixin(self, name, arity): + return self._get_callable(self._mixins, name, arity) + + def set_mixin(self, name, arity, cb): + self._assert_mutable() + self._set_callable(self._mixins, name, arity, cb) + + def function(self, name, arity): + return self._get_callable(self._functions, name, arity) + + def set_function(self, name, arity, cb): + self._assert_mutable() + self._set_callable(self._functions, name, arity, cb) diff --git a/aip_site/vendor/pyscss/scss/rule.py b/aip_site/vendor/pyscss/scss/rule.py new file mode 100644 index 0000000..32dd74a --- /dev/null +++ b/aip_site/vendor/pyscss/scss/rule.py @@ -0,0 +1,366 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import logging +import re + +from scss.namespace import Namespace + +log = logging.getLogger(__name__) + +SORTED_SELECTORS = False + +sort = sorted if SORTED_SELECTORS else lambda it: it + + +def extend_unique(seq, more): + """Return a new sequence containing the items in `seq` plus any items in + `more` that aren't already in `seq`, preserving the order of both. + """ + seen = set(seq) + new = [] + for item in more: + if item not in seen: + seen.add(item) + new.append(item) + + return seq + type(seq)(new) + + +class SassRule(object): + """At its heart, a CSS rule: combination of a selector and zero or more + properties. But this is Sass, so it also tracks some Sass-flavored + metadata, like `@extend` rules and `@media` nesting. + """ + + 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(), + ancestry=None, + nested=0, + from_source_file=None, from_lineno=0): + + self.from_source_file = from_source_file + self.from_lineno = from_lineno + + self.source_file = source_file + 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 {} + self.extends_selectors = extends_selectors + + if namespace is None: + assert False + self.namespace = Namespace() + else: + self.namespace = namespace + + if properties is None: + self.properties = [] + else: + self.properties = properties + + if ancestry is None: + self.ancestry = RuleAncestry() + else: + self.ancestry = ancestry + + self.nested = nested + + self.descendants = 0 + + def __repr__(self): + # TODO probably want to encode this with string_escape on python 2, and + # similar elsewhere, especially since this file has unicode_literals + return "" % ( + self.ancestry, + len(self.properties), + ) + + @property + def selectors(self): + # TEMPORARY + if self.ancestry.headers and self.ancestry.headers[-1].is_selector: + return self.ancestry.headers[-1].selectors + else: + return () + + @property + def file_and_line(self): + """Return the filename and line number where this rule originally + appears, in the form "foo.scss:3". Used for error messages. + """ + ret = "%s:%d" % (self.source_file.path, self.lineno) + if self.from_source_file: + ret += " (%s:%d)" % (self.from_source_file.path, self.from_lineno) + return ret + + @property + def is_empty(self): + """Return whether this rule is considered "empty" -- i.e., has no + contents that should end up in the final CSS. + """ + if self.properties: + # Rules containing CSS properties are never empty + return False + + if not self.descendants: + for header in self.ancestry.headers: + if header.is_atrule and header.directive != '@media': + # At-rules should always be preserved, UNLESS they are @media + # blocks, which are known to be noise if they don't have any + # contents of their own + return False + + return True + + @property + def is_pure_placeholder(self): + selectors = self.selectors + if not selectors: + return False + for s in selectors: + if not s.has_placeholder: + return False + return True + + + def copy(self): + return type(self)( + source_file=self.source_file, + lineno=self.lineno, + + from_source_file=self.from_source_file, + from_lineno=self.from_lineno, + + unparsed_contents=self.unparsed_contents, + + legacy_compiler_options=self.legacy_compiler_options, + options=self.options, + #properties=list(self.properties), + properties=self.properties, + extends_selectors=self.extends_selectors, + #ancestry=list(self.ancestry), + ancestry=self.ancestry, + + namespace=self.namespace.derive(), + nested=self.nested, + ) + + +class RuleAncestry(object): + def __init__(self, headers=()): + self.headers = tuple(headers) + + def __repr__(self): + return "<%s %r>" % (type(self).__name__, self.headers) + + def __len__(self): + return len(self.headers) + + def with_nested_selectors(self, c_selectors): + if self.headers and self.headers[-1].is_selector: + # Need to merge with parent selectors + p_selectors = self.headers[-1].selectors + + new_selectors = [] + for p_selector in p_selectors: + for c_selector in c_selectors: + new_selectors.append(c_selector.with_parent(p_selector)) + + # Replace the last header with the new merged selectors + new_headers = self.headers[:-1] + (BlockSelectorHeader(new_selectors),) + return RuleAncestry(new_headers) + + else: + # Whoops, no parent selectors. Just need to double-check that + # there are no uses of `&`. + for c_selector in c_selectors: + if c_selector.has_parent_reference: + raise ValueError("Can't use parent selector '&' in top-level rules") + + # Add the children as a new header + new_headers = self.headers + (BlockSelectorHeader(c_selectors),) + return RuleAncestry(new_headers) + + def with_more_selectors(self, selectors): + """Return a new ancestry that also matches the given selectors. No + nesting is done. + """ + if self.headers and self.headers[-1].is_selector: + new_selectors = extend_unique( + self.headers[-1].selectors, + selectors) + new_headers = self.headers[:-1] + ( + BlockSelectorHeader(new_selectors),) + return RuleAncestry(new_headers) + else: + new_headers = self.headers + (BlockSelectorHeader(selectors),) + return RuleAncestry(new_headers) + + +class BlockHeader(object): + """...""" + # TODO doc me depending on how UnparsedBlock is handled... + + is_atrule = False + is_scope = False + is_selector = False + + @classmethod + def parse(cls, prop, has_contents=False): + num_lines = prop.count('\n') + prop = prop.strip() + + # Simple pre-processing + if prop.startswith('+') and not has_contents: + # Expand '+' at the beginning of a rule as @include. But not if + # there's a block, because that's probably a CSS selector. + # DEVIATION: this is some semi hybrid of Sass and xCSS syntax + prop = '@include ' + prop[1:] + try: + if '(' not in prop or prop.index(':') < prop.index('('): + prop = prop.replace(':', '(', 1) + if '(' in prop: + prop += ')' + except ValueError: + pass + elif prop.startswith('='): + # Expand '=' at the beginning of a rule as @mixin + prop = '@mixin ' + prop[1:] + elif prop.startswith('@prototype '): + # Remove '@prototype ' + # TODO what is @prototype?? + prop = prop[11:] + + # Minor parsing + if prop.startswith('@'): + # This pattern MUST NOT BE ABLE TO FAIL! + # This is slightly more lax than the CSS syntax technically allows, + # e.g. identifiers aren't supposed to begin with three hyphens. + # But we don't care, and will just spit it back out anyway. + m = re.match( + '@(else if|[-_a-zA-Z0-9\U00000080-\U0010FFFF]*)\\b', + prop, re.I) + directive = m.group(0).lower() + argument = prop[len(directive):].strip() + if not argument: + argument = None + 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, num_lines) + else: + return BlockSelectorHeader(prop, num_lines) + + +class BlockAtRuleHeader(BlockHeader): + is_atrule = True + + 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) + + def render(self): + if self.argument: + return "%s %s" % (self.directive, self.argument) + else: + return self.directive + + +class BlockSelectorHeader(BlockHeader): + is_selector = True + + 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) + + def render(self, sep=', ', super_selector=''): + return sep.join(sort( + super_selector + s.render() + for s in self.selectors + if not s.has_placeholder)) + + +class BlockScopeHeader(BlockHeader): + is_scope = True + + def __init__(self, scope, unscoped_value, num_lines=0): + self.scope = scope + + if unscoped_value: + self.unscoped_value = 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. + + At the top level, CSS (and Sass) documents consist of a sequence of blocks. + A block may be a ruleset: + + selector { block; block; block... } + + Or it may be an @-rule: + + @rule arguments { block; block; block... } + + Or it may be only a single property declaration: + + property: value + + pyScss's first parsing pass breaks the document into these blocks, and each + block becomes an instance of this class. + """ + + def __init__(self, parent_rule, lineno, prop, unparsed_contents): + self.parent_rule = parent_rule + self.header = BlockHeader.parse(prop, has_contents=bool(unparsed_contents)) + + # Basic properties + self.lineno = ( + parent_rule.lineno - parent_rule.num_header_lines + lineno - 1) + self.prop = prop + self.unparsed_contents = unparsed_contents + + @property + def directive(self): + return self.header.directive + + @property + def argument(self): + return self.header.argument + + ### What kind of thing is this? + + @property + def is_atrule(self): + return self.header.is_atrule + + @property + def is_scope(self): + return self.header.is_scope diff --git a/aip_site/vendor/pyscss/scss/scss_meta.py b/aip_site/vendor/pyscss/scss/scss_meta.py new file mode 100644 index 0000000..059eb78 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/scss_meta.py @@ -0,0 +1,59 @@ +#-*- coding: utf-8 -*- +""" +pyScss, a Scss compiler for Python + +@author German M. Bravo (Kronuz) +@version 1.4.0 +@see https://github.com/Kronuz/pyScss +@copyright (c) 2012-2013 German M. Bravo (Kronuz) +@license MIT License + http://www.opensource.org/licenses/mit-license.php + +pyScss compiles Scss, a superset of CSS that is more powerful, elegant and +easier to maintain than plain-vanilla CSS. The library acts as a CSS source code +preprocesor which allows you to use variables, nested rules, mixins, andhave +inheritance of rules, all with a CSS-compatible syntax which the preprocessor +then compiles to standard CSS. + +Scss, as an extension of CSS, helps keep large stylesheets well-organized. It +borrows concepts and functionality from projects such as OOCSS and other similar +frameworks like as Sass. It's build on top of the original PHP xCSS codebase +structure but it's been completely rewritten, many bugs have been fixed and it +has been extensively extended to support almost the full range of Sass' Scss +syntax and functionality. + +Bits of code in pyScss come from various projects: +Compass: + (c) 2009 Christopher M. Eppstein + http://compass-style.org/ +Sass: + (c) 2006-2009 Hampton Catlin and Nathan Weizenbaum + http://sass-lang.com/ +xCSS: + (c) 2010 Anton Pawlik + http://xcss.antpaw.org/docs/ + + This file defines Meta data, according to PEP314 + (http://www.python.org/dev/peps/pep-0314/) which is common to both pyScss + and setup.py distutils. + + We create this here so this information can be compatible with BOTH + Python 2.x and Python 3.x so setup.py can use it when building pyScss + for both Py3.x and Py2.x + +""" +from __future__ import unicode_literals + +import sys + +VERSION_INFO = (1, 4, 0) +DATE_INFO = (2022, 2, 23) # YEAR, MONTH, DAY +VERSION = '.'.join(str(i) for i in VERSION_INFO) +REVISION = '%04d%02d%02d' % DATE_INFO +BUILD_INFO = "pyScss v" + VERSION + " (" + REVISION + ")" +AUTHOR = "German M. Bravo (Kronuz)" +AUTHOR_EMAIL = 'german.mb@gmail.com' +URL = 'http://github.com/Kronuz/pyScss' +DOWNLOAD_URL = 'http://github.com/Kronuz/pyScss/tarball/v' + VERSION +LICENSE = "MIT" +PROJECT = "pyScss" diff --git a/aip_site/vendor/pyscss/scss/selector.py b/aip_site/vendor/pyscss/scss/selector.py new file mode 100644 index 0000000..c187f01 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/selector.py @@ -0,0 +1,664 @@ +from __future__ import print_function + +import re +from warnings import warn + +from scss.cssdefs import CSS2_PSEUDO_ELEMENTS + +# Super dumb little selector parser. + +# Yes, yes, this is a regex tokenizer. The actual meaning of the +# selector doesn't matter; the parts are just important for matching up +# during @extend. + +# Selectors have three levels: simple, combinator, comma-delimited. +# Each combinator can only appear once as a delimiter between simple +# selectors, so it can be thought of as a prefix. +# So this: +# a.b + c, d#e +# parses into two Selectors with these structures: +# [[' ', 'a', '.b'], ['+', 'c']] +# [[' ', 'd', '#e']] +# Note that the first simple selector has an implied descendant +# combinator -- i.e., it is a descendant of the root element. +# TODO `*html` is incorrectly parsed as a single selector +# TODO this oughta be touched up for css4 selectors +SELECTOR_TOKENIZER = re.compile(r''' + # Colons introduce pseudo-selectors, sometimes with parens + # TODO doesn't handle quoted ) + [:]+ [-\w]+ (?: [(] .+? [)] )? + + # These guys are combinators -- note that a single space counts too + | \s* [ +>~,] \s* + + # Square brackets are attribute tests + # TODO: this doesn't handle ] within a string + | \[ .+? \] + + # Dot and pound start class/id selectors. Percent starts a Sass + # extend-target faux selector. + | [.#%] [-\w]+ + + # Percentages are used for @keyframes + | [-.\d]+ [%] + + # Plain identifiers, or single asterisks, are element names + | [-\w]+ + | [*] + + # & is the sass replacement token + | [&] + + # And as a last-ditch effort, just eat up to whitespace + | (\S+) +''', re.VERBOSE | re.MULTILINE) + + +# Set of starting squiggles that are known to mean "this is a general +# commutative token", i.e., not an element +BODY_TOKEN_SIGILS = frozenset('#.[:%') + + +def _is_combinator_subset_of(specific, general, is_first=True): + """Return whether `specific` matches a non-strict subset of what `general` + matches. + """ + if is_first and general == ' ': + # First selector always has a space to mean "descendent of root", which + # still holds if any other selector appears above it + return True + + if specific == general: + return True + + if specific == '>' and general == ' ': + return True + + if specific == '+' and general == '~': + return True + + return False + + +class SimpleSelector(object): + """A simple selector, by CSS 2.1 terminology: a combination of element + name, class selectors, id selectors, and other criteria that all apply to a + single element. + + Note that CSS 3 considers EACH of those parts to be a "simple selector", + and calls a group of them a "sequence of simple selectors". That's a + terrible class name, so we're going with 2.1 here. + + For lack of a better name, each of the individual parts is merely called a + "token". + + Note that it's possible to have zero tokens. This isn't legal CSS, but + it's perfectly legal Sass, since you might nest blocks like so: + + body > { + div { + ... + } + } + """ + def __init__(self, combinator, tokens): + self.combinator = combinator + # TODO enforce that only one element name (including *) appears in a + # selector. only one pseudo, too. + # TODO remove duplicates? + self.tokens = tuple(tokens) + + def __repr__(self): + return "<%s: %r>" % (type(self).__name__, self.render()) + + def __hash__(self): + return hash((self.combinator, self.tokens)) + + def __eq__(self, other): + if not isinstance(other, SimpleSelector): + return NotImplemented + + return ( + self.combinator == other.combinator and + self.tokens == other.tokens) + + @property + def has_parent_reference(self): + return '&' in self.tokens or 'self' in self.tokens + + @property + def has_placeholder(self): + for token in self.tokens: + if token.startswith('%'): + return True + return False + + def is_superset_of(self, other, soft_combinator=False): + """Return True iff this selector matches the same elements as `other`, + and perhaps others. + + That is, ``.foo`` is a superset of ``.foo.bar``, because the latter is + more specific. + + Set `soft_combinator` true to ignore the specific case of this selector + having a descendent combinator and `other` having anything else. This + is for superset checking for ``@extend``, where a space combinator + really means "none". + """ + # Combinators must match, OR be compatible -- space is a superset of >, + # ~ is a superset of + + if soft_combinator and self.combinator == ' ': + combinator_superset = True + else: + combinator_superset = ( + self.combinator == other.combinator or + (self.combinator == ' ' and other.combinator == '>') or + (self.combinator == '~' and other.combinator == '+')) + + return ( + combinator_superset and + set(self.tokens) <= set(other.tokens)) + + def replace_parent(self, parent_simples): + """If ``&`` (or the legacy xCSS equivalent ``self``) appears in this + selector, replace it with the given iterable of parent selectors. + + Returns a tuple of simple selectors. + """ + assert parent_simples + + ancestors = parent_simples[:-1] + parent = parent_simples[-1] + + did_replace = False + new_tokens = [] + for token in self.tokens: + if not did_replace and token in ('&', 'self'): + did_replace = True + new_tokens.extend(parent.tokens) + if token == 'self': + warn(FutureWarning( + "The xCSS 'self' selector is deprecated and will be " + "removed in 2.0. Use & instead. ({0!r})" + .format(self) + )) + else: + new_tokens.append(token) + + if not did_replace: + # This simple selector doesn't contain a parent reference so just + # stick it on the end + return parent_simples + (self,) + + # This simple selector was merged into the direct parent. + merged_self = type(self)(parent.combinator, new_tokens) + selector = ancestors + (merged_self,) + # Our combinator goes on the first ancestor, i.e., substituting "foo + # bar baz" into "+ &.quux" produces "+ foo bar baz.quux". This means a + # potential conflict with the first ancestor's combinator! + root = selector[0] + if not _is_combinator_subset_of(self.combinator, root.combinator): + raise ValueError( + "Can't sub parent {0!r} into {1!r}: " + "combinators {2!r} and {3!r} conflict!" + .format( + parent_simples, self, self.combinator, root.combinator)) + + root = type(self)(self.combinator, root.tokens) + selector = (root,) + selector[1:] + return tuple(selector) + + def merge_into(self, other): + """Merge two simple selectors together. This is expected to be the + selector being injected into `other` -- that is, `other` is the + selector for a block using ``@extend``, and `self` is a selector being + extended. + + Element tokens must come first, and pseudo-element tokens must come + last, and there can only be one of each. The final selector thus looks + something like:: + + [element] [misc self tokens] [misc other tokens] [pseudo-element] + + This method does not check for duplicate tokens; those are assumed to + have been removed earlier, during the search for a hinge. + """ + # TODO it shouldn't be possible to merge two elements or two pseudo + # elements, /but/ it shouldn't just be a fatal error here -- it + # shouldn't even be considered a candidate for extending! + # TODO this is slightly inconsistent with ruby, which treats a trailing + # set of self tokens like ':before.foo' as a single unit to be stuck at + # the end. but that's completely bogus anyway. + element = [] + middle = [] + pseudo = [] + for token in self.tokens + other.tokens: + if token in CSS2_PSEUDO_ELEMENTS or token.startswith('::'): + pseudo.append(token) + elif token[0] in BODY_TOKEN_SIGILS: + middle.append(token) + else: + element.append(token) + new_tokens = element + middle + pseudo + + if self.combinator == ' ' or self.combinator == other.combinator: + combinator = other.combinator + elif other.combinator == ' ': + combinator = self.combinator + else: + raise ValueError( + "Don't know how to merge conflicting combinators: " + "{0!r} and {1!r}" + .format(self, other)) + return type(self)(combinator, new_tokens) + + def difference(self, other): + new_tokens = tuple(token for token in self.tokens if token not in set(other.tokens)) + return type(self)(self.combinator, new_tokens) + + def render(self): + # TODO fail if there are no tokens, or if one is a placeholder? + rendered = ''.join(self.tokens) + if self.combinator == ' ': + return rendered + elif rendered: + return self.combinator + ' ' + rendered + else: + return self.combinator + + +class Selector(object): + """A single CSS selector.""" + + def __init__(self, simples): + """Return a selector containing a sequence of `SimpleSelector`s. + + You probably want to use `parse_many` or `parse_one` instead. + """ + # TODO enforce uniqueness + self.simple_selectors = tuple(simples) + + @classmethod + def parse_many(cls, selector): + selector = selector.strip() + ret = [] + + pending = dict( + simples=[], + combinator=' ', + tokens=[], + ) + + def promote_simple(): + if pending['tokens']: + pending['simples'].append( + SimpleSelector(pending['combinator'], pending['tokens'])) + pending['combinator'] = ' ' + pending['tokens'] = [] + + def promote_selector(): + promote_simple() + if pending['combinator'] != ' ': + pending['simples'].append( + SimpleSelector(pending['combinator'], [])) + pending['combinator'] = ' ' + if pending['simples']: + ret.append(cls(pending['simples'])) + pending['simples'] = [] + + pos = 0 + while pos < len(selector): + # TODO i don't think this deals with " + " correctly. anywhere. + # TODO this used to turn "1.5%" into empty string; why does error + # not work? + m = SELECTOR_TOKENIZER.match(selector, pos) + if not m: + # TODO prettify me + raise SyntaxError("Couldn't parse selector: %r" % (selector,)) + + token = m.group(0) + pos += len(token) + + # Kill any extraneous space, BUT make sure not to turn a lone space + # into an empty string + token = token.strip() or ' ' + + if token == ',': + # End current selector + promote_selector() + elif token in ' +>~': + # End current simple selector + promote_simple() + pending['combinator'] = token + else: + # Add to pending simple selector + pending['tokens'].append(token) + + # Deal with any remaining pending bits + promote_selector() + + return ret + + @classmethod + def parse_one(cls, selector_string): + selectors = cls.parse_many(selector_string) + if len(selectors) != 1: + # TODO better error + raise ValueError + + return selectors[0] + + def __repr__(self): + return "<%s: %r>" % (type(self).__name__, self.render()) + + def __hash__(self): + return hash(self.simple_selectors) + + def __eq__(self, other): + if not isinstance(other, Selector): + return NotImplemented + + return self.simple_selectors == other.simple_selectors + + @property + def has_parent_reference(self): + for simple in self.simple_selectors: + if simple.has_parent_reference: + return True + return False + + @property + def has_placeholder(self): + for simple in self.simple_selectors: + if simple.has_placeholder: + return True + return False + + def with_parent(self, parent): + saw_parent_ref = False + + new_simples = [] + for simple in self.simple_selectors: + if simple.has_parent_reference: + new_simples.extend(simple.replace_parent(parent.simple_selectors)) + saw_parent_ref = True + else: + new_simples.append(simple) + + if not saw_parent_ref: + new_simples = parent.simple_selectors + tuple(new_simples) + + return type(self)(new_simples) + + def lookup_key(self): + """Build a key from the "important" parts of a selector: elements, + classes, ids. + """ + parts = set() + for node in self.simple_selectors: + for token in node.tokens: + if token[0] not in ':[': + parts.add(token) + + if not parts: + # Should always have at least ONE key; selectors with no elements, + # no classes, and no ids can be indexed as None to avoid a scan of + # every selector in the entire document + parts.add(None) + + return frozenset(parts) + + def is_superset_of(self, other): + assert isinstance(other, Selector) + + idx = 0 + for other_node in other.simple_selectors: + if idx >= len(self.simple_selectors): + return False + + while idx < len(self.simple_selectors): + node = self.simple_selectors[idx] + idx += 1 + + if node.is_superset_of(other_node): + break + + return True + + def substitute(self, target, replacement): + """Return a list of selectors obtained by replacing the `target` + selector with `replacement`. + + Herein lie the guts of the Sass @extend directive. + + In general, for a selector ``a X b Y c``, a target ``X Y``, and a + replacement ``q Z``, return the selectors ``a q X b Z c`` and ``q a X b + Z c``. Note in particular that no more than two selectors will be + returned, and the permutation of ancestors will never insert new simple + selectors "inside" the target selector. + """ + # Find the target in the parent selector, and split it into + # before/after + p_before, p_extras, p_after = self.break_around(target.simple_selectors) + + # The replacement has no hinge; it only has the most specific simple + # selector (which is the part that replaces "self" in the parent) and + # whatever preceding simple selectors there may be + r_trail = replacement.simple_selectors[:-1] + r_extras = replacement.simple_selectors[-1] + + # TODO what if the prefix doesn't match? who wins? should we even get + # this far? + focal_nodes = (p_extras.merge_into(r_extras),) + + befores = _merge_selectors(p_before, r_trail) + + cls = type(self) + return [ + cls(before + focal_nodes + p_after) + for before in befores] + + def break_around(self, hinge): + """Given a simple selector node contained within this one (a "hinge"), + break it in half and return a parent selector, extra specifiers for the + hinge, and a child selector. + + That is, given a hinge X, break the selector A + X.y B into A, + .y, + and B. + """ + hinge_start = hinge[0] + for i, node in enumerate(self.simple_selectors): + # In this particular case, a ' ' combinator actually means "no" (or + # any) combinator, so it should be ignored + if hinge_start.is_superset_of(node, soft_combinator=True): + start_idx = i + break + else: + raise ValueError( + "Couldn't find hinge %r in compound selector %r" % + (hinge_start, self.simple_selectors)) + + for i, hinge_node in enumerate(hinge): + if i == 0: + # We just did this + continue + + self_node = self.simple_selectors[start_idx + i] + if hinge_node.is_superset_of(self_node): + continue + + # TODO this isn't true; consider finding `a b` in `a c a b` + raise ValueError( + "Couldn't find hinge %r in compound selector %r" % + (hinge_node, self.simple_selectors)) + + end_idx = start_idx + len(hinge) - 1 + + focal_node = self.simple_selectors[end_idx] + extras = focal_node.difference(hinge[-1]) + + return ( + self.simple_selectors[:start_idx], + extras, + self.simple_selectors[end_idx + 1:]) + + def render(self): + return ' '.join(simple.render() for simple in self.simple_selectors) + + +def _merge_selectors(left, right): + """Given two selector chains (lists of simple selectors), return a list of + selector chains representing elements matched by both of them. + + This operation is not exact, and involves some degree of fudging -- the + wackier and more divergent the input, the more fudging. It's meant to be + what a human might expect rather than a precise covering of all possible + cases. Most notably, when the two input chains have absolutely nothing in + common, the output is merely ``left + right`` and ``right + left`` rather + than all possible interleavings. + """ + + if not left or not right: + # At least one is empty, so there are no conflicts; just return + # whichever isn't empty. Remember to return a LIST, though + return [left or right] + + lcs = longest_common_subsequence(left, right, _merge_simple_selectors) + + ret = [()] # start with a dummy empty chain or weaving won't work + + left_last = 0 + right_last = 0 + for left_next, right_next, merged in lcs: + ret = _weave_conflicting_selectors( + ret, + left[left_last:left_next], + right[right_last:right_next], + (merged,)) + + left_last = left_next + 1 + right_last = right_next + 1 + + ret = _weave_conflicting_selectors( + ret, + left[left_last:], + right[right_last:]) + + return ret + + +def _weave_conflicting_selectors(prefixes, a, b, suffix=()): + """Part of the selector merge algorithm above. Not useful on its own. Pay + no attention to the man behind the curtain. + """ + # OK, what this actually does: given a list of selector chains, two + # "conflicting" selector chains, and an optional suffix, return a new list + # of chains like this: + # prefix[0] + a + b + suffix, + # prefix[0] + b + a + suffix, + # prefix[1] + a + b + suffix, + # ... + # In other words, this just appends a new chain to each of a list of given + # chains, except that the new chain might be the superposition of two + # other incompatible chains. + both = a and b + for prefix in prefixes: + yield prefix + a + b + suffix + if both: + # Only use both orderings if there's an actual conflict! + yield prefix + b + a + suffix + + +def _merge_simple_selectors(a, b): + """Merge two simple selectors, for the purposes of the LCS algorithm below. + + In practice this returns the more specific selector if one is a subset of + the other, else it returns None. + """ + # TODO what about combinators + if a.is_superset_of(b): + return b + elif b.is_superset_of(a): + return a + else: + return None + + +def longest_common_subsequence(a, b, mergefunc=None): + """Find the longest common subsequence between two iterables. + + The longest common subsequence is the core of any diff algorithm: it's the + longest sequence of elements that appears in both parent sequences in the + same order, but NOT necessarily consecutively. + + Original algorithm borrowed from Wikipedia: + http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Code_for_the_dynamic_programming_solution + + This function is used only to implement @extend, largely because that's + what the Ruby implementation does. Thus it's been extended slightly from + the simple diff-friendly algorithm given above. + + What @extend wants to know is whether two simple selectors are compatible, + not just equal. To that end, you must pass in a "merge" function to + compare a pair of elements manually. It should return `None` if they are + incompatible, and a MERGED element if they are compatible -- in the case of + selectors, this is whichever one is more specific. + + Because of this fuzzier notion of equality, the return value is a list of + ``(a_index, b_index, value)`` tuples rather than items alone. + """ + if mergefunc is None: + # Stupid default, just in case + def mergefunc(a, b): + if a == b: + return a + return None + + # Precalculate equality, since it can be a tad expensive and every pair is + # compared at least once + eq = {} + for ai, aval in enumerate(a): + for bi, bval in enumerate(b): + eq[ai, bi] = mergefunc(aval, bval) + + # Build the "length" matrix, which provides the length of the LCS for + # arbitrary-length prefixes. -1 exists only to support the base case + prefix_lcs_length = {} + for ai in range(-1, len(a)): + for bi in range(-1, len(b)): + if ai == -1 or bi == -1: + l = 0 + elif eq[ai, bi]: + l = prefix_lcs_length[ai - 1, bi - 1] + 1 + else: + l = max( + prefix_lcs_length[ai, bi - 1], + prefix_lcs_length[ai - 1, bi]) + + prefix_lcs_length[ai, bi] = l + + # The interesting part. The key insight is that the bottom-right value in + # the length matrix must be the length of the LCS because of how the matrix + # is defined, so all that's left to do is backtrack from the ends of both + # sequences in whatever way keeps the LCS as long as possible, and keep + # track of the equal pairs of elements we see along the way. + # Wikipedia does this with recursion, but the algorithm is trivial to + # rewrite as a loop, as below. + ai = len(a) - 1 + bi = len(b) - 1 + + ret = [] + while ai >= 0 and bi >= 0: + merged = eq[ai, bi] + if merged is not None: + ret.append((ai, bi, merged)) + ai -= 1 + bi -= 1 + elif prefix_lcs_length[ai, bi - 1] > prefix_lcs_length[ai - 1, bi]: + bi -= 1 + else: + ai -= 1 + + # ret has the latest items first, which is backwards + ret.reverse() + return ret diff --git a/aip_site/vendor/pyscss/scss/setup.py b/aip_site/vendor/pyscss/scss/setup.py new file mode 100644 index 0000000..7112a18 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/setup.py @@ -0,0 +1,14 @@ +from distutils.core import setup, Extension + +setup(name='pyScss', + version='1.1.1', + description='pyScss', + ext_modules=[ + Extension( + '_scss', + sources=['src/_scss.c', 'src/block_locator.c', 'src/scanner.c'], + libraries=['pcre'], + optional=True + ) + ] +) diff --git a/aip_site/vendor/pyscss/scss/source.py b/aip_site/vendor/pyscss/scss/source.py new file mode 100644 index 0000000..bb1f1ec --- /dev/null +++ b/aip_site/vendor/pyscss/scss/source.py @@ -0,0 +1,376 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division + +import hashlib +import logging +from pathlib import Path +import re + +import six + +from scss.cssdefs import ( + _ml_comment_re, _sl_comment_re, + _collapse_properties_space_re, + _strings_re, _urls_re, +) +from scss.cssdefs import determine_encoding + + +log = logging.getLogger(__name__) + + +_safe_strings = { + '^doubleslash^': '//', + '^bigcopen^': '/*', + '^bigcclose^': '*/', + '^doubledot^': ':', + '^semicolon^': ';', + '^curlybracketopen^': '{', + '^curlybracketclosed^': '}', +} +_reverse_safe_strings = dict((v, k) for k, v in _safe_strings.items()) +_safe_strings_re = re.compile('|'.join(map(re.escape, _safe_strings))) +_reverse_safe_strings_re = re.compile('|'.join( + map(re.escape, _reverse_safe_strings))) + + +class MISSING(object): + def __repr__(self): + return "" +MISSING = MISSING() + + +# TODO i'm still not entirely happy with this, nor with the concept of an +# "origin". it should really be a "loader", with a defined API. also, even +# with all these helpful classmethods, i'm still having to do a lot of manual +# mucking around in django-pyscss, where all i'm given is a file path and a +# string of the contents, and i want to /not/ re-read the file. +class SourceFile(object): + """A single input file to be fed to the compiler. Detects the encoding + (according to CSS spec rules) and performs some light pre-processing. + + This class is mostly internal and you shouldn't have to worry about it. + + Source files are uniquely identified by their ``.key``, a 2-tuple of + ``(origin, relpath)``. + + ``origin`` is an object from the compiler's search + path, most often a directory represented by a :class:`pathlib.Path`. + ``relpath`` is a relative path from there to the actual file, again usually + a ``Path``. + + The idea here is that source files don't always actually come from the + filesystem, yet import semantics are expressed in terms of paths. By + keeping the origin and relative path separate, it's possible for e.g. + Django to swap in an object that has the ``Path`` interface, but actually + looks for files in an arbitrary storage backend. In that case it would + make no sense to key files by their absolute path, as they may not exist on + disk or even on the same machine. Also, relative imports can then continue + to work, because they're guaranteed to only try the same origin. + + The ``origin`` may thus be anything that implements a minimal ``Path``ish + interface (division operator, ``.parent``, ``.resolve()``). It may also be + ``None``, indicating that the file came from a string or some other origin + that can't usefully produce other files. + + ``relpath``, however, should always be a ``Path``. or string. XXX only when origin (There's little + advantage to making it anything else.) A ``relpath`` may **never** contain + ".."; there is nothing above the origin. + + Note that one minor caveat of this setup is that it's possible for the same + file on disk to be imported under two different names (even though symlinks + are always resolved), if directories in the search path happen to overlap. + """ + + key = None + """A 2-tuple of ``(origin, relpath)`` that uniquely identifies where the + file came from and how to find its siblings. + """ + + def __init__( + self, origin, relpath, contents, encoding=None, + is_sass=None): + """Not normally used. See the three alternative constructors: + :func:`SourceFile.from_file`, :func:`SourceFile.from_path`, and + :func:`SourceFile.from_string`. + """ + if not isinstance(contents, six.text_type): + raise TypeError( + "Expected text for 'contents', got {0}" + .format(type(contents))) + + if origin and '..' in relpath.parts: + raise ValueError( + "relpath cannot contain ..: {0!r}".format(relpath)) + + self.origin = origin + self.relpath = relpath + self.key = origin, relpath + + self.encoding = encoding + if is_sass is None: + # TODO autodetect from the contents if the extension is bogus + # or missing? + if origin: + self.is_sass = relpath.suffix == '.sass' + else: + self.is_sass = False + else: + self.is_sass = is_sass + self.contents = self.prepare_source(contents) + + @property + def path(self): + """Concatenation of ``origin`` and ``relpath``, as a string. Used in + stack traces and other debugging places. + """ + if self.origin: + return six.text_type(self.origin / self.relpath) + else: + return six.text_type(self.relpath) + + def __repr__(self): + return "<{0} {1!r} from {2!r}>".format( + type(self).__name__, self.relpath, self.origin) + + def __hash__(self): + return hash(self.key) + + def __eq__(self, other): + if self is other: + return True + + if not isinstance(other, SourceFile): + return NotImplemented + + return self.key == other.key + + def __ne__(self, other): + return not self == other + + @classmethod + def _key_from_path(cls, path, origin=MISSING): + # Given an origin (which may be MISSING) and an absolute path, + # return a key. + if origin is MISSING: + # Resolve only the parent, in case the file itself is a symlink + origin = path.parent.resolve() + relpath = Path(path.name) + else: + # Again, resolving the origin is fine; we just don't want to + # resolve anything inside it, lest we ruin some intended symlink + # structure + origin = origin.resolve() + # pathlib balks if this requires lexically ascending <3 + relpath = path.relative_to(origin) + + return origin, relpath + + @classmethod + def read(cls, origin, relpath, **kwargs): + """Read a source file from an ``(origin, relpath)`` tuple, as would + happen from an ``@import`` statement. + """ + path = origin / relpath + with path.open('rb') as f: + return cls.from_file(f, origin, relpath, **kwargs) + + @classmethod + def from_path(cls, path, origin=MISSING, **kwargs): + """Read Sass source from a :class:`pathlib.Path`. + + If no origin is given, it's assumed to be the file's parent directory. + """ + origin, relpath = cls._key_from_path(path, origin) + + # Open in binary mode so we can reliably detect the encoding + with path.open('rb') as f: + return cls.from_file(f, origin, relpath, **kwargs) + + # back-compat + @classmethod + def from_filename(cls, path_string, origin=MISSING, **kwargs): + """ Read Sass source from a String specifying the path + """ + path = Path(path_string) + return cls.from_path(path, origin, **kwargs) + + @classmethod + def from_file(cls, f, origin=MISSING, relpath=MISSING, **kwargs): + """Read Sass source from a file or file-like object. + + If `origin` or `relpath` are missing, they are derived from the file's + ``.name`` attribute as with `from_path`. If it doesn't have one, the + origin becomes None and the relpath becomes the file's repr. + """ + contents = f.read() + encoding = determine_encoding(contents) + if isinstance(contents, six.binary_type): + contents = contents.decode(encoding) + + if origin is MISSING or relpath is MISSING: + filename = getattr(f, 'name', None) + if filename is None: + origin = None + relpath = repr(f) + else: + origin, relpath = cls._key_from_path(Path(filename), origin) + + return cls(origin, relpath, contents, encoding=encoding, **kwargs) + + @classmethod + def from_string(cls, string, relpath=None, encoding=None, is_sass=None): + """Read Sass source from the contents of a string. + + The origin is always None. `relpath` defaults to "string:...". + """ + if isinstance(string, six.text_type): + # Already decoded; we don't know what encoding to use for output, + # though, so still check for a @charset. + # TODO what if the given encoding conflicts with the one in the + # file? do we care? + if encoding is None: + encoding = determine_encoding(string) + + byte_contents = string.encode(encoding) + text_contents = string + elif isinstance(string, six.binary_type): + encoding = determine_encoding(string) + byte_contents = string + text_contents = string.decode(encoding) + else: + raise TypeError("Expected text or bytes, got {0!r}".format(string)) + + origin = None + if relpath is None: + m = hashlib.sha256() + m.update(byte_contents) + relpath = repr("string:{0}:{1}".format( + m.hexdigest()[:16], text_contents[:100])) + + return cls( + origin, relpath, text_contents, encoding=encoding, + is_sass=is_sass, + ) + + def parse_scss_line(self, line, state): + ret = '' + + if line is None: + line = '' + + line = state['line_buffer'] + line + + if line and line[-1] == '\\': + state['line_buffer'] = line[:-1] + return '' + else: + state['line_buffer'] = '' + + output = state['prev_line'] + output = output.strip() + + state['prev_line'] = line + + ret += output + ret += '\n' + return ret + + def parse_sass_line(self, line, state): + ret = '' + + if line is None: + line = '' + + line = state['line_buffer'] + line + + if line and line[-1] == '\\': + state['line_buffer'] = line[:-1] + return ret + else: + state['line_buffer'] = '' + + indent = len(line) - len(line.lstrip()) + + # make sure we support multi-space indent as long as indent is + # consistent + if indent and not state['indent_marker']: + state['indent_marker'] = indent + + if state['indent_marker']: + indent //= state['indent_marker'] + + if indent == state['prev_indent']: + # same indentation as previous line + if state['prev_line']: + state['prev_line'] += ';' + elif indent > state['prev_indent']: + # new indentation is greater than previous, we just entered a new + # block + state['prev_line'] += ' {' + state['nested_blocks'] += 1 + else: + # indentation is reset, we exited a block + block_diff = state['prev_indent'] - indent + if state['prev_line']: + state['prev_line'] += ';' + state['prev_line'] += ' }' * block_diff + state['nested_blocks'] -= block_diff + + output = state['prev_line'] + output = output.strip() + + state['prev_indent'] = indent + state['prev_line'] = line + + ret += output + ret += '\n' + return ret + + def prepare_source(self, codestr, sass=False): + state = { + 'line_buffer': '', + 'prev_line': '', + 'prev_indent': 0, + 'nested_blocks': 0, + 'indent_marker': 0, + } + if self.is_sass: + parse_line = self.parse_sass_line + else: + parse_line = self.parse_scss_line + _codestr = codestr + codestr = '' + for line in _codestr.splitlines(): + codestr += parse_line(line, state) + # 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( + lambda n: _reverse_safe_strings[n.group(0)], m.group(0)), + codestr) + codestr = _urls_re.sub( + lambda m: _reverse_safe_strings_re.sub( + lambda n: _reverse_safe_strings[n.group(0)], m.group(0)), + codestr) + + # removes multiple line comments + codestr = _ml_comment_re.sub('', codestr) + + # removes inline comments, but not :// (protocol) + codestr = _sl_comment_re.sub('', codestr) + + codestr = _safe_strings_re.sub( + lambda m: _safe_strings[m.group(0)], codestr) + + # collapse the space in properties blocks + codestr = _collapse_properties_space_re.sub(r'\1{', codestr) + + return codestr diff --git a/aip_site/vendor/pyscss/scss/tool.py b/aip_site/vendor/pyscss/scss/tool.py new file mode 100644 index 0000000..f9b83b4 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/tool.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python +from __future__ import absolute_import +from __future__ import print_function + +from collections import deque +from contextlib import contextmanager +import logging +import os +import re +import sys + +from scss import config +from scss.calculator import Calculator +from scss.compiler import _prop_split_re +from scss.compiler import Compiler +from scss.errors import SassEvaluationError +from scss.legacy import Scss +from scss.legacy import _default_scss_vars +from scss.namespace import Namespace +from scss.rule import SassRule +from scss.rule import UnparsedBlock +from scss.scss_meta import BUILD_INFO +from scss.source import SourceFile +from scss.util import profiling + +try: + raw_input +except NameError: + raw_input = input + +log = logging.getLogger(__name__) +logging.getLogger('scss').setLevel(logging.INFO) + + +def main(): + logging.basicConfig(format="%(levelname)s: %(message)s") + + from optparse import OptionGroup, OptionParser, SUPPRESS_HELP + + if hasattr(config.LOAD_PATHS, 'split'): + initial_load_paths = [p.strip() for p in config.LOAD_PATHS.split(',')] + else: + initial_load_paths = list(config.LOAD_PATHS) + + def append_load_path(option, opt_str, value, parser): + dest = getattr(parser.values, option.dest) + paths = value.replace(os.pathsep, ',').replace(';', ',').split(',') + for path in paths: + path = path.strip() + if path and path not in dest: + dest.append(path) + + parser = OptionParser(usage="Usage: %prog [options] [file]", + description="Converts Scss files to CSS.", + add_help_option=False) + parser.add_option("-i", "--interactive", action="store_true", + help="Run an interactive Scss shell") + parser.add_option("-w", "--watch", metavar="DIR", + help="Watch the files in DIR, and recompile when they change") + parser.add_option("-r", "--recursive", action="store_true", default=False, + help="Also watch directories inside of the watch directory") + parser.add_option("-o", "--output", metavar="PATH", + help="Write output to PATH (a directory if using watch, a file otherwise)") + parser.add_option("-s", "--suffix", metavar="STRING", + help="If using watch, a suffix added to the output filename (i.e. filename.STRING.css)") + parser.add_option("--time", action="store_true", + help="Ignored, will be removed in 2.0") + parser.add_option("--debug-info", action="store_true", + help="Turns on scss's debugging information") + parser.add_option("--no-debug-info", action="store_false", + dest="debug_info", default=False, + help="Turns off scss's debugging information") + parser.add_option("-T", "--test", action="store_true", help=SUPPRESS_HELP) + parser.add_option("-t", "--style", metavar="NAME", + dest="style", default='nested', + help="Output style. Can be nested (default), compact, compressed, or expanded.") + parser.add_option("-C", "--no-compress", action="store_false", dest="style", default=True, + help="Don't minify outputted CSS") + parser.add_option("-?", action="help", help=SUPPRESS_HELP) + parser.add_option("-h", "--help", action="help", + help="Show this message and exit") + parser.add_option("-v", "--version", action="store_true", + help="Print version and exit") + + paths_group = OptionGroup(parser, "Resource Paths") + paths_group.add_option("-I", "--load-path", metavar="PATH", type="string", + action="callback", callback=append_load_path, dest="load_paths", + default=initial_load_paths, + help="Add a scss import path, may be given multiple times") + paths_group.add_option("-S", "--static-root", metavar="PATH", dest="static_root", + help="Static root path (Where images and static resources are located)") + paths_group.add_option("-A", "--assets-root", metavar="PATH", dest="assets_root", + help="Assets root path (Sprite images will be created here)") + paths_group.add_option("-a", "--assets-url", metavar="URL", dest="assets_url", + help="URL to reach the files in your assets_root") + paths_group.add_option("-F", "--fonts-root", metavar="PATH", dest="fonts_root", + help="Fonts root path (Where fonts are located)") + paths_group.add_option("-f", "--fonts-url", metavar="PATH", dest="fonts_url", + help="URL to reach the fonts in your fonts_root") + paths_group.add_option("--images-root", metavar="PATH", dest="images_root", + help="Images root path (Where images are located)") + paths_group.add_option("--images-url", metavar="PATH", dest="images_url", + help="URL to reach the images in your images_root") + paths_group.add_option("--cache-root", metavar="PATH", dest="cache_root", + help="Cache root path (Cache files will be created here)") + parser.add_option_group(paths_group) + + parser.add_option("--sass", action="store_true", + dest="is_sass", default=None, + help="Sass mode") + + options, args = parser.parse_args() + + # General runtime configuration + if options.static_root is not None: + config.STATIC_ROOT = options.static_root + if options.assets_root is not None: + config.ASSETS_ROOT = options.assets_root + + if options.fonts_root is not None: + config.FONTS_ROOT = options.fonts_root + if options.fonts_url is not None: + config.FONTS_URL = options.fonts_url + + if options.images_root is not None: + config.IMAGES_ROOT = options.images_root + if options.images_url is not None: + config.IMAGES_URL = options.images_url + + if options.cache_root is not None: + config.CACHE_ROOT = options.cache_root + if options.assets_url is not None: + config.ASSETS_URL = options.assets_url + + # Execution modes + if options.test: + run_tests() + elif options.version: + print_version() + elif options.interactive: + run_repl(options) + elif options.watch: + watch_sources(options) + else: + do_build(options, args) + + +def print_version(): + print(BUILD_INFO) + + +def run_tests(): + try: + import pytest + except ImportError: + raise ImportError("You need py.test installed to run the test suite.") + pytest.main("") # don't let py.test re-consume our arguments + + +def do_build(options, args): + if options.output is not None: + out = open(options.output, 'wb') + else: + out = sys.stdout + # Get the unencoded stream on Python 3 + out = getattr(out, 'buffer', out) + + css = Scss(scss_opts={ + 'style': options.style, + 'debug_info': options.debug_info, + }, + search_paths=options.load_paths, + ) + if not args: + args = ['-'] + source_files = [] + for path in args: + if path == '-': + source = SourceFile.from_file(sys.stdin, relpath="", is_sass=options.is_sass) + else: + source = SourceFile.from_filename(path, is_sass=options.is_sass) + source_files.append(source) + + encodings = set(source.encoding for source in source_files) + if len(encodings) > 1: + sys.stderr.write( + "Can't combine these files! " + "They have different encodings: {0}\n" + .format(', '.join(encodings)) + ) + sys.exit(3) + + output = css.compile(source_files=source_files) + out.write(output.encode(source_files[0].encoding)) + + for f, t in profiling.items(): + sys.stderr.write("%s took %03fs" % (f, t)) + + +def watch_sources(options): + import time + try: + from watchdog.observers import Observer + from watchdog.events import PatternMatchingEventHandler + except ImportError: + sys.stderr.write("Using watch functionality requires the `watchdog` library: http://pypi.python.org/pypi/watchdog/") + sys.exit(1) + if options.output and not os.path.isdir(options.output): + sys.stderr.write("watch file output directory is invalid: '%s'" % (options.output)) + sys.exit(2) + + class ScssEventHandler(PatternMatchingEventHandler): + def __init__(self, *args, **kwargs): + super(ScssEventHandler, self).__init__(*args, **kwargs) + self.css = Scss(scss_opts={ + 'style': options.style, + 'debug_info': options.debug_info, + }, + search_paths=options.load_paths, + ) + self.output = options.output + self.suffix = options.suffix + + def is_valid(self, path): + return os.path.isfile(path) and (path.endswith('.scss') or path.endswith('.sass')) and not os.path.basename(path).startswith('_') + + def process(self, path): + if os.path.isdir(path): + for f in os.listdir(path): + full = os.path.join(path, f) + if self.is_valid(full): + self.compile(full) + elif self.is_valid(path): + self.compile(path) + + def compile(self, src_path): + fname = os.path.basename(src_path) + if fname.endswith('.scss') or fname.endswith('.sass'): + fname = fname[:-5] + if self.suffix: + fname += '.' + self.suffix + fname += '.css' + else: + # you didn't give me a file of the correct type! + return False + + if self.output: + dest_path = os.path.join(self.output, fname) + else: + dest_path = os.path.join(os.path.dirname(src_path), fname) + + print("Compiling %s => %s" % (src_path, dest_path)) + dest_file = open(dest_path, 'wb') + dest_file.write(self.css.compile(scss_file=src_path).encode('utf-8')) + + def on_moved(self, event): + super(ScssEventHandler, self).on_moved(event) + self.process(event.dest_path) + + def on_created(self, event): + super(ScssEventHandler, self).on_created(event) + self.process(event.src_path) + + def on_modified(self, event): + super(ScssEventHandler, self).on_modified(event) + self.process(event.src_path) + + event_handler = ScssEventHandler(patterns=['*.scss', '*.sass']) + observer = Observer() + observer.schedule(event_handler, path=options.watch, recursive=options.recursive) + observer.start() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + + +@contextmanager +def readline_history(fn): + try: + import readline + except ImportError: + yield + return + + try: + readline.read_history_file(fn) + except IOError: + pass + + try: + yield + finally: + try: + readline.write_history_file(fn) + except IOError: + pass + + +def run_repl(is_sass=False): + repl = SassRepl() + + with readline_history(os.path.expanduser('~/.scss-history')): + print("Welcome to %s interactive shell" % (BUILD_INFO,)) + while True: + try: + in_ = raw_input('>>> ').strip() + for output in repl(in_): + print(output) + except (EOFError, KeyboardInterrupt): + print("Bye!") + return + + +class SassRepl(object): + def __init__(self, is_sass=False): + # TODO it would be lovely to get these out of here, somehow + self.namespace = Namespace(variables=_default_scss_vars) + + self.compiler = Compiler(namespace=self.namespace) + self.compilation = self.compiler.make_compilation() + self.legacy_compiler_options = {} + self.source_file = SourceFile.from_string('', '', is_sass=is_sass) + self.calculator = Calculator(self.namespace) + + def __call__(self, s): + # TODO this is kind of invasive; surely it's possible to do this + # without calling only private methods + from pprint import pformat + + if s in ('exit', 'quit'): + raise KeyboardInterrupt + + for s in s.split(';'): + s = self.source_file.prepare_source(s.strip()) + if not s: + continue + elif s.startswith('@'): + scope = None + properties = [] + children = deque() + rule = SassRule(self.source_file, namespace=self.namespace, legacy_compiler_options=self.legacy_compiler_options, properties=properties) + block = UnparsedBlock(rule, 1, s, None) + code, name = (s.split(None, 1) + [''])[:2] + if code == '@option': + self.compilation._at_options(self.calculator, rule, scope, block) + continue + elif code == '@import': + # TODO this doesn't really work either since there's no path + self.compilation._at_import(self.calculator, rule, scope, block) + continue + elif code == '@include': + final_cont = '' + self.compilation._at_include(self.calculator, rule, scope, block) + code = self.compilation._print_properties(properties).rstrip('\n') + if code: + final_cont += code + if children: + # TODO this almost certainly doesn't work, and is kind of goofy anyway since @mixin isn't supported + self.compilation.children.extendleft(children) + self.compilation.parse_children() + code = self.compilation._create_css(self.compilation.rules).rstrip('\n') + if code: + final_cont += code + yield final_cont + continue + elif s == 'ls' or s.startswith('show(') or s.startswith('show ') or s.startswith('ls(') or s.startswith('ls '): + m = re.match(r'(?:show|ls)(\()?\s*([^,/\\) ]*)(?:[,/\\ ]([^,/\\ )]+))*(?(1)\))', s, re.IGNORECASE) + if m: + name = m.group(2) + code = m.group(3) + name = name and name.strip().rstrip('s') # remove last 's' as in functions + code = code and code.strip() + ns = self.namespace + if not name: + yield pformat(list(sorted(['vars', 'options', 'mixins', 'functions']))) + elif name in ('v', 'var', 'variable'): + variables = dict(ns._variables) + if code == '*': + pass + elif code: + variables = dict((k, v) for k, v in variables.items() if code in k) + else: + variables = dict((k, v) for k, v in variables.items() if not k.startswith('$--')) + yield pformat(variables) + + elif name in ('o', 'opt', 'option'): + opts = self.legacy_compiler_options + if code == '*': + pass + elif code: + opts = dict((k, v) for k, v in opts.items() if code in k) + else: + opts = dict((k, v) for k, v in opts.items()) + yield pformat(opts) + + elif name in ('m', 'mix', 'mixin', 'f', 'func', 'funct', 'function'): + if name.startswith('m'): + funcs = dict(ns._mixins) + elif name.startswith('f'): + funcs = dict(ns._functions) + if code == '*': + pass + elif code: + funcs = dict((k, v) for k, v in funcs.items() if code in k[0]) + else: + pass + # TODO print source when possible + yield pformat(funcs) + continue + elif s.startswith('$') and (':' in s or '=' in s): + prop, value = [a.strip() for a in _prop_split_re.split(s, 1)] + prop = self.calculator.do_glob_math(prop) + value = self.calculator.calculate(value) + self.namespace.set_variable(prop, value) + continue + + # TODO respect compress? + try: + yield(self.calculator.calculate(s).render()) + except (SyntaxError, SassEvaluationError) as e: + print("%s" % e, file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/aip_site/vendor/pyscss/scss/types.py b/aip_site/vendor/pyscss/scss/types.py new file mode 100644 index 0000000..acaf72c --- /dev/null +++ b/aip_site/vendor/pyscss/scss/types.py @@ -0,0 +1,1343 @@ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable +import colorsys +from fractions import Fraction +import operator +import re +import string +from warnings import warn + +import six + +from scss.cssdefs import COLOR_LOOKUP, COLOR_NAMES, ZEROABLE_UNITS, convert_units_to_base_units, cancel_base_units, count_base_units + +PRECISION = 5 + + +############################################################################### +# pyScss data types: + +# TODO make Value work as a string in every way? i.e. have a .quotes... +class Value(object): + is_null = False + sass_type_name = 'unknown' + + def __repr__(self): + return "<{0}: {1!r}>".format(type(self).__name__, self.value) + + # Sass values are all true, except for booleans and nulls + def __bool__(self): + return True + + def __nonzero__(self): + # Py 2's name for __bool__ + return self.__bool__() + + # All Sass scalars also act like one-element spaced lists + use_comma = False + + def __iter__(self): + return iter((self,)) + + def __len__(self): + return 1 + + def __getitem__(self, key): + if key not in (-1, 0): + raise IndexError(key) + + return self + + def __contains__(self, item): + return self == item + + ### NOTE: From here on down, the operators are exposed to Sass code and + ### thus should ONLY return Sass types + + # Reasonable default for equality + def __eq__(self, other): + return Boolean( + type(self) == type(other) and self.value == other.value) + + def __ne__(self, other): + return Boolean(not self.__eq__(other)) + + # Only numbers support ordering + def __lt__(self, other): + raise TypeError("Can't compare %r with %r" % (self, other)) + + def __le__(self, other): + raise TypeError("Can't compare %r with %r" % (self, other)) + + def __gt__(self, other): + raise TypeError("Can't compare %r with %r" % (self, other)) + + def __ge__(self, other): + raise TypeError("Can't compare %r with %r" % (self, other)) + + # Math ops + def __add__(self, other): + # Default behavior is to treat both sides like strings + if isinstance(other, String): + return String(self.render() + other.value, quotes=other.quotes) + return String(self.render() + other.render()) + + def __sub__(self, other): + # Default behavior is to treat the whole expression like one string + return String.unquoted(self.render() + "-" + other.render()) + + def __div__(self, other): + return String.unquoted(self.render() + "/" + other.render()) + + # Sass types have no notion of floor vs true division + def __truediv__(self, other): + return self.__div__(other) + + def __floordiv__(self, other): + return self.__div__(other) + + def __mul__(self, other): + return NotImplemented + + def __pos__(self): + return String("+" + self.render()) + + def __neg__(self): + return String("-" + self.render()) + + def to_dict(self): + """Return the Python dict equivalent of this map. + + If this type can't be expressed as a map, raise. + """ + return dict(self.to_pairs()) + + def to_pairs(self): + """Return the Python list-of-tuples equivalent of this map. Note that + this is different from ``self.to_dict().items()``, because Sass maps + preserve order. + + If this type can't be expressed as a map, raise. + """ + raise ValueError("Not a map: {0!r}".format(self)) + + def render(self, compress=False): + """Return this value's CSS representation as a string (text, i.e. + unicode!). + + If `compress` is true, try hard to shorten the string at the cost of + readability. + """ + raise NotImplementedError + + def render_interpolated(self, compress=False): + """Return this value's string representation as appropriate for + returning from an interpolation. + """ + return self.render(compress) + + +class Null(Value): + is_null = True + sass_type_name = 'null' + + def __init__(self, value=None): + pass + + def __str__(self): + return self.sass_type_name + + def __repr__(self): + return "<{0}>".format(type(self).__name__) + + def __hash__(self): + return hash(None) + + def __bool__(self): + return False + + def __eq__(self, other): + return Boolean(isinstance(other, Null)) + + def __ne__(self, other): + return Boolean(not self.__eq__(other)) + + def render(self, compress=False): + return self.sass_type_name + + def render_interpolated(self, compress=False): + # Interpolating a null gives you nothing. + return '' + + +class Undefined(Null): + sass_type_name = 'undefined' + + def __init__(self, value=None): + pass + + def __add__(self, other): + return self + + def __radd__(self, other): + return self + + def __sub__(self, other): + return self + + def __rsub__(self, other): + return self + + def __div__(self, other): + return self + + def __rdiv__(self, other): + return self + + def __truediv__(self, other): + return self + + def __rtruediv__(self, other): + return self + + def __floordiv__(self, other): + return self + + def __rfloordiv__(self, other): + return self + + def __mul__(self, other): + return self + + def __rmul__(self, other): + return self + + def __pos__(self): + return self + + def __neg__(self): + return self + + +class Boolean(Value): + sass_type_name = 'bool' + + def __init__(self, value): + self.value = bool(value) + + def __str__(self): + return 'true' if self.value else 'false' + + def __hash__(self): + return hash(self.value) + + def __bool__(self): + return self.value + + def render(self, compress=False): + if self.value: + return 'true' + else: + return 'false' + + +class Number(Value): + sass_type_name = 'number' + + def __init__(self, amount, unit=None, unit_numer=(), unit_denom=()): + if isinstance(amount, Number): + assert not unit and not unit_numer and not unit_denom + self.value = amount.value + self.unit_numer = amount.unit_numer + self.unit_denom = amount.unit_denom + return + + # Numbers with units are stored internally as a "base" unit, which can + # involve float division, which can lead to precision errors in obscure + # cases. Storing the original units would only partially solve this + # problem, because there'd still be a possible loss of precision when + # converting in Sass-land. Almost all of the conversion factors are + # simple ratios of small whole numbers, so using Fraction across the + # board preserves as much precision as possible. + # TODO in fact, i wouldn't mind parsing Sass values as fractions of a + # power of ten! + # TODO this slowed the test suite down by about 10%, ha + if isinstance(amount, (int, float)): + amount = Fraction.from_float(amount) + elif isinstance(amount, Fraction): + pass + else: + raise TypeError("Expected number, got %r" % (amount,)) + + if unit is not None: + unit_numer = unit_numer + (unit.lower(),) + + # Cancel out any convertable units on the top and bottom + numerator_base_units = count_base_units(unit_numer) + denominator_base_units = count_base_units(unit_denom) + + # Count which base units appear both on top and bottom + cancelable_base_units = {} + for unit, count in numerator_base_units.items(): + cancelable_base_units[unit] = min( + count, denominator_base_units.get(unit, 0)) + + # Actually remove the units + numer_factor, unit_numer = cancel_base_units(unit_numer, cancelable_base_units) + denom_factor, unit_denom = cancel_base_units(unit_denom, cancelable_base_units) + + # And we're done + self.unit_numer = tuple(unit_numer) + self.unit_denom = tuple(unit_denom) + self.value = amount * (numer_factor / denom_factor) + + def __repr__(self): + value = self.value + int_value = int(value) + if value == int_value: + value = int_value + + full_unit = ' * '.join(self.unit_numer) + if self.unit_denom: + full_unit += ' / ' + full_unit += ' * '.join(self.unit_denom) + + if full_unit: + full_unit = ' ' + full_unit + + return "<{0} {1}{2}>".format(type(self).__name__, value, full_unit) + + def __hash__(self): + return hash((self.value, self.unit_numer, self.unit_denom)) + + def __int__(self): + return int(self.value) + + def __float__(self): + return float(self.value) + + def __pos__(self): + return self + + def __neg__(self): + return self * Number(-1) + + def __str__(self): + return self.render() + + def __eq__(self, other): + if not isinstance(other, Number): + return Boolean(False) + return self._compare(other, operator.__eq__, soft_fail=True) + + def __lt__(self, other): + return self._compare(other, operator.__lt__) + + def __le__(self, other): + return self._compare(other, operator.__le__) + + def __gt__(self, other): + return self._compare(other, operator.__gt__) + + def __ge__(self, other): + return self._compare(other, operator.__ge__) + + def _compare(self, other, op, soft_fail=False): + if not isinstance(other, Number): + raise TypeError("Can't compare %r and %r" % (self, other)) + + # A unitless operand is treated as though it had the other operand's + # units, and zero values can cast to anything, so in both cases the + # units can be ignored + if (self.is_unitless or other.is_unitless or + self.value == 0 or other.value == 0): + left = self + right = other + else: + left = self.to_base_units() + right = other.to_base_units() + + if left.unit_numer != right.unit_numer or left.unit_denom != right.unit_denom: + if soft_fail: + # Used for equality only, where == should never fail + return Boolean(False) + else: + raise ValueError("Can't reconcile units: %r and %r" % (self, other)) + + return Boolean(op(round(left.value, PRECISION), round(right.value, PRECISION))) + + def __pow__(self, exp): + if not isinstance(exp, Number): + raise TypeError("Can't raise %r to power %r" % (self, exp)) + if not exp.is_unitless: + raise TypeError("Exponent %r cannot have units" % (exp,)) + + if self.is_unitless: + return Number(self.value ** exp.value) + + # Units can only be exponentiated to integral powers -- what's the + # square root of 'px'? (Well, it's sqrt(px), but supporting that is + # a bit out of scope.) + if exp.value != int(exp.value): + raise ValueError("Can't raise units of %r to non-integral power %r" % (self, exp)) + + return Number( + self.value ** int(exp.value), + unit_numer=self.unit_numer * int(exp.value), + unit_denom=self.unit_denom * int(exp.value), + ) + + def __mul__(self, other): + if not isinstance(other, Number): + return NotImplemented + + amount = self.value * other.value + numer = self.unit_numer + other.unit_numer + denom = self.unit_denom + other.unit_denom + + return Number(amount, unit_numer=numer, unit_denom=denom) + + def __div__(self, other): + if not isinstance(other, Number): + return NotImplemented + + amount = self.value / other.value + numer = self.unit_numer + other.unit_denom + denom = self.unit_denom + other.unit_numer + + return Number(amount, unit_numer=numer, unit_denom=denom) + + def __mod__(self, other): + if not isinstance(other, Number): + return NotImplemented + + amount = self.value % other.value + + if self.is_unitless: + return Number(amount) + + if not other.is_unitless: + left = self.to_base_units() + right = other.to_base_units() + + if left.unit_numer != right.unit_numer or left.unit_denom != right.unit_denom: + raise ValueError("Can't reconcile units: %r and %r" % (self, other)) + + return Number(amount, unit_numer=self.unit_numer, unit_denom=self.unit_denom) + + def __add__(self, other): + # Numbers auto-cast to strings when added to other strings + if isinstance(other, String): + return String(self.render(), quotes=None) + other + + return self._add_sub(other, operator.add) + + def __sub__(self, other): + return self._add_sub(other, operator.sub) + + def _add_sub(self, other, op): + """Implements both addition and subtraction.""" + if not isinstance(other, Number): + return NotImplemented + + # If either side is unitless, inherit the other side's units. Skip all + # the rest of the conversion math, too. + if self.is_unitless or other.is_unitless: + return Number( + op(self.value, other.value), + unit_numer=self.unit_numer or other.unit_numer, + unit_denom=self.unit_denom or other.unit_denom, + ) + + # Likewise, if either side is zero, it can auto-cast to any units + if self.value == 0: + return Number( + op(self.value, other.value), + unit_numer=other.unit_numer, + unit_denom=other.unit_denom, + ) + elif other.value == 0: + return Number( + op(self.value, other.value), + unit_numer=self.unit_numer, + unit_denom=self.unit_denom, + ) + + # Reduce both operands to the same units + left = self.to_base_units() + right = other.to_base_units() + + if left.unit_numer != right.unit_numer or left.unit_denom != right.unit_denom: + raise ValueError("Can't reconcile units: %r and %r" % (self, other)) + + new_amount = op(left.value, right.value) + + # Convert back to the left side's units + if left.value != 0: + new_amount = new_amount * self.value / left.value + + return Number(new_amount, unit_numer=self.unit_numer, unit_denom=self.unit_denom) + + ### Helper methods, mostly used internally + + def to_base_units(self): + """Convert to a fixed set of "base" units. The particular units are + arbitrary; what's important is that they're consistent. + + Used for addition and comparisons. + """ + # Convert to "standard" units, as defined by the conversions dict above + amount = self.value + + numer_factor, numer_units = convert_units_to_base_units(self.unit_numer) + denom_factor, denom_units = convert_units_to_base_units(self.unit_denom) + + return Number( + amount * numer_factor / denom_factor, + unit_numer=numer_units, + unit_denom=denom_units, + ) + + ### Utilities for public consumption + + @classmethod + def wrap_python_function(cls, fn): + """Wraps an unary Python math function, translating the argument from + Sass to Python on the way in, and vice versa for the return value. + + Used to wrap simple Python functions like `ceil`, `floor`, etc. + """ + def wrapped(sass_arg): + # TODO enforce no units for trig? + python_arg = sass_arg.value + python_ret = fn(python_arg) + sass_ret = cls( + python_ret, + unit_numer=sass_arg.unit_numer, + unit_denom=sass_arg.unit_denom) + return sass_ret + + return wrapped + + def to_python_index(self, length, check_bounds=True, circular=False): + """Return a plain Python integer appropriate for indexing a sequence of + the given length. Raise if this is impossible for any reason + whatsoever. + """ + if not self.is_unitless: + raise ValueError("Index cannot have units: {0!r}".format(self)) + + ret = int(self.value) + if ret != self.value: + raise ValueError("Index must be an integer: {0!r}".format(ret)) + + if ret == 0: + raise ValueError("Index cannot be zero") + + if check_bounds and not circular and abs(ret) > length: + raise ValueError("Index {0!r} out of bounds for length {1}".format(ret, length)) + + if ret > 0: + ret -= 1 + + if circular: + ret = ret % length + + return ret + + @property + def has_simple_unit(self): + """Returns True iff the unit is expressible in CSS, i.e., has no + denominator and at most one unit in the numerator. + """ + return len(self.unit_numer) <= 1 and not self.unit_denom + + def is_simple_unit(self, unit): + """Return True iff the unit is simple (as above) and matches the given + unit. + """ + if self.unit_denom or len(self.unit_numer) > 1: + return False + + if not self.unit_numer: + # Empty string historically means no unit + return unit == '' + + return self.unit_numer[0] == unit + + @property + def is_unitless(self): + return not self.unit_numer and not self.unit_denom + + def render(self, compress=False): + if not self.has_simple_unit: + raise ValueError("Can't express compound units in CSS: %r" % (self,)) + + if self.unit_numer: + unit = self.unit_numer[0] + else: + unit = '' + + value = self.value + if compress and unit in ZEROABLE_UNITS and value == 0: + return '0' + + if value == 0: # -0.0 is plain 0 + value = 0 + + val = ('%%0.0%df' % PRECISION) % round(value, PRECISION) + val = val.rstrip('0').rstrip('.') + + if compress and val.startswith('0.'): + # Strip off leading zero when compressing + val = val[1:] + + return val + unit + + +class List(Value): + """A list of other values. May be delimited by commas or spaces. + + Lists of one item don't make much sense in CSS, but can exist in Sass. Use ...... + + Lists may also contain zero items, but these are forbidden from appearing + in CSS output. + """ + + sass_type_name = 'list' + + def __init__(self, iterable, separator=None, use_comma=None, literal=False): + if isinstance(iterable, List): + iterable = iterable.value + + if (not isinstance(iterable, Iterable) or + isinstance(iterable, six.string_types)): + raise TypeError("Expected list, got %r" % (iterable,)) + + self.value = list(iterable) + + for item in self.value: + if not isinstance(item, Value): + raise TypeError("Expected a Sass type, got %r" % (item,)) + + # TODO remove separator argument entirely + if use_comma is None: + self.use_comma = separator == "," + else: + self.use_comma = use_comma + + self.literal = literal + + @classmethod + def maybe_new(cls, values, use_comma=True): + """If `values` contains only one item, return that item. Otherwise, + return a List as normal. + """ + if len(values) == 1: + return values[0] + else: + return cls(values, use_comma=use_comma) + + def maybe(self): + """If this List contains only one item, return it. Otherwise, return + the List. + """ + if len(self.value) == 1: + return self.value[0] + else: + return self + + @classmethod + def from_maybe(cls, values, use_comma=True): + """If `values` appears to not be a list, return a list containing it. + Otherwise, return a List as normal. + """ + if values is None: + values = [] + return values + + @classmethod + def from_maybe_starargs(cls, args, use_comma=True): + """If `args` has one element which appears to be a list, return it. + Otherwise, return a list as normal. + + Mainly used by Sass function implementations that predate `...` + support, so they can accept both a list of arguments and a single list + stored in a variable. + """ + if len(args) == 1: + if isinstance(args[0], cls): + return args[0] + elif isinstance(args[0], (list, tuple)): + return cls(args[0], use_comma=use_comma) + + return cls(args, use_comma=use_comma) + + def __repr__(self): + return "<{0} {1}>".format( + type(self).__name__, + self.delimiter().join(repr(item) for item in self), + ) + + def __hash__(self): + return hash((tuple(self.value), self.use_comma)) + + def delimiter(self, compress=False): + if self.use_comma: + if compress: + return ',' + else: + return ', ' + else: + return ' ' + + def __len__(self): + return len(self.value) + + def __str__(self): + return self.render() + + def __iter__(self): + return iter(self.value) + + def __contains__(self, item): + return item in self.value + + def __getitem__(self, key): + return self.value[key] + + def to_pairs(self): + pairs = [] + for item in self: + if len(item) != 2: + return super(List, self).to_pairs() + + pairs.append(tuple(item)) + + return pairs + + def render(self, compress=False): + if not self.value: + raise ValueError("Can't render empty list as CSS") + + delim = self.delimiter(compress) + + if self.literal: + value = self.value + else: + # Non-literal lists have nulls stripped + value = [item for item in self.value if not item.is_null] + # Non-empty lists containing only nulls become nothing, just like + # single nulls + if not value: + return '' + + return delim.join( + item.render(compress=compress) + for item in value + ) + + def render_interpolated(self, compress=False): + return self.delimiter(compress).join( + item.render_interpolated(compress) for item in self) + + # DEVIATION: binary ops on lists and scalars act element-wise + def __add__(self, other): + if isinstance(other, List): + max_list, min_list = (self, other) if len(self) > len(other) else (other, self) + return List([item + max_list[i] for i, item in enumerate(min_list)], use_comma=self.use_comma) + + elif isinstance(other, String): + # UN-DEVIATION: adding a string should fall back to canonical + # behavior of string addition + return super(List, self).__add__(other) + + else: + return List([item + other for item in self], use_comma=self.use_comma) + + def __sub__(self, other): + if isinstance(other, List): + max_list, min_list = (self, other) if len(self) > len(other) else (other, self) + return List([item - max_list[i] for i, item in enumerate(min_list)], use_comma=self.use_comma) + + return List([item - other for item in self], use_comma=self.use_comma) + + def __mul__(self, other): + if isinstance(other, List): + max_list, min_list = (self, other) if len(self) > len(other) else (other, self) + max_list, min_list = (self, other) if len(self) > len(other) else (other, self) + return List([item * max_list[i] for i, item in enumerate(min_list)], use_comma=self.use_comma) + + return List([item * other for item in self], use_comma=self.use_comma) + + def __div__(self, other): + if isinstance(other, List): + max_list, min_list = (self, other) if len(self) > len(other) else (other, self) + return List([item / max_list[i] for i, item in enumerate(min_list)], use_comma=self.use_comma) + + return List([item / other for item in self], use_comma=self.use_comma) + + def __pos__(self): + return self + + def __neg__(self): + return List([-item for item in self], use_comma=self.use_comma) + + +class Arglist(List): + """An argument list. Acts mostly like a list, with keyword arguments sort + of tacked on separately, and only accessible via Python (or the Sass + `keywords` function). + """ + sass_type_name = 'arglist' + keywords_retrieved = False + + def __init__(self, args, kwargs): + self._kwargs = Map(kwargs) + super(Arglist, self).__init__(args, use_comma=True) + + def extract_keywords(self): + self.keywords_retrieved = True + return self._kwargs + + +def _constrain(value, lb=0, ub=1): + """Helper for Color constructors. Constrains a value to a range.""" + if value < lb: + return lb + elif value > ub: + return ub + else: + return value + + +class Color(Value): + sass_type_name = 'color' + original_literal = None + + def __init__(self, tokens): + self.tokens = tokens + self.value = (0, 0, 0, 1) + if tokens is None: + self.value = (0, 0, 0, 1) + elif isinstance(tokens, Color): + self.value = tokens.value + else: + raise TypeError("Can't make Color from %r" % (tokens,)) + + ### Alternate constructors + + @classmethod + def from_rgb(cls, red, green, blue, alpha=1.0, original_literal=None): + red = _constrain(red) + green = _constrain(green) + blue = _constrain(blue) + alpha = _constrain(alpha) + + self = cls.__new__(cls) # TODO + self.tokens = None + # TODO really should store these things internally as 0-1, but can't + # until stuff stops examining .value directly + self.value = (red * 255.0, green * 255.0, blue * 255.0, alpha) + + if original_literal is not None: + self.original_literal = original_literal + + return self + + @classmethod + def from_hsl(cls, hue, saturation, lightness, alpha=1.0): + hue = _constrain(hue) + saturation = _constrain(saturation) + lightness = _constrain(lightness) + alpha = _constrain(alpha) + + r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation) + return cls.from_rgb(r, g, b, alpha) + + @classmethod + def from_hex(cls, hex_string, literal=False): + if not hex_string.startswith('#'): + raise ValueError("Expected #abcdef, got %r" % (hex_string,)) + + if literal: + original_literal = hex_string + else: + original_literal = None + + hex_string = hex_string[1:] + + # Always include the alpha channel + if len(hex_string) == 3: + hex_string += 'f' + elif len(hex_string) == 6: + hex_string += 'ff' + + # Now there should be only two possibilities. Normalize to a list of + # two hex digits + if len(hex_string) == 4: + chunks = [ch * 2 for ch in hex_string] + elif len(hex_string) == 8: + chunks = [ + hex_string[0:2], hex_string[2:4], hex_string[4:6], hex_string[6:8] + ] + + rgba = [int(ch, 16) / 255 for ch in chunks] + return cls.from_rgb(*rgba, original_literal=original_literal) + + @classmethod + def from_name(cls, name): + """Build a Color from a CSS color name.""" + self = cls.__new__(cls) # TODO + self.original_literal = name + + r, g, b, a = COLOR_NAMES[name] + + self.value = r, g, b, a + return self + + ### Accessors + + @property + def rgb(self): + # TODO: deprecate, relies on internals + return tuple(self.value[:3]) + + @property + def rgba(self): + return ( + self.value[0] / 255, + self.value[1] / 255, + self.value[2] / 255, + self.value[3], + ) + + @property + def hsl(self): + rgba = self.rgba + h, l, s = colorsys.rgb_to_hls(*rgba[:3]) + return h, s, l + + @property + def alpha(self): + return self.value[3] + + @property + def rgba255(self): + return ( + int(self.value[0] * 1 + 0.5), + int(self.value[1] * 1 + 0.5), + int(self.value[2] * 1 + 0.5), + int(self.value[3] * 255 + 0.5), + ) + + def __repr__(self): + return "<{0} {1}>".format(type(self).__name__, self.render()) + + def __hash__(self): + return hash(self.value) + + def __eq__(self, other): + if not isinstance(other, Color): + return Boolean(False) + + # Scale channels to 255 and round to integers; this allows only 8-bit + # color, but Ruby sass makes the same assumption, and otherwise it's + # easy to get lots of float errors for HSL colors. + left = tuple(round(n) for n in self.rgba255) + right = tuple(round(n) for n in other.rgba255) + return Boolean(left == right) + + def __add__(self, other): + if isinstance(other, (Color, Number)): + return self._operate(other, operator.add) + else: + return super(Color, self).__add__(other) + + def __sub__(self, other): + if isinstance(other, (Color, Number)): + return self._operate(other, operator.sub) + else: + return super(Color, self).__sub__(other) + + def __mul__(self, other): + if isinstance(other, (Color, Number)): + return self._operate(other, operator.mul) + else: + return super(Color, self).__mul__(other) + + def __div__(self, other): + if isinstance(other, (Color, Number)): + return self._operate(other, operator.div) + else: + return super(Color, self).__div__(other) + + def _operate(self, other, op): + if isinstance(other, Number): + if not other.is_unitless: + raise ValueError("Expected unitless Number, got %r" % (other,)) + + other_rgb = (other.value,) * 3 + elif isinstance(other, Color): + if self.alpha != other.alpha: + raise ValueError("Alpha channels must match between %r and %r" + % (self, other)) + + other_rgb = other.rgb + else: + raise TypeError("Expected Color or Number, got %r" % (other,)) + + new_rgb = [ + min(255., max(0., op(left, right))) + # for from_rgb + / 255. + for (left, right) in zip(self.rgb, other_rgb) + ] + + return Color.from_rgb(*new_rgb, alpha=self.alpha) + + def render(self, compress=False): + """Return a rendered representation of the color. If `compress` is + true, the shortest possible representation is used; otherwise, named + colors are rendered as names and all others are rendered as hex (or + with the rgba function). + """ + + if not compress and self.original_literal: + return self.original_literal + + candidates = [] + + # TODO this assumes CSS resolution is 8-bit per channel, but so does + # Ruby. + r, g, b, a = self.value + r, g, b = int(round(r)), int(round(g)), int(round(b)) + + # Build a candidate list in order of preference. If `compress` is + # True, the shortest candidate is used; otherwise, the first candidate + # is used. + + # Try color name + key = r, g, b, a + if key in COLOR_LOOKUP: + candidates.append(COLOR_LOOKUP[key]) + + if a == 1: + # Hex is always shorter than function notation + if all(ch % 17 == 0 for ch in (r, g, b)): + candidates.append("#%1x%1x%1x" % (r // 17, g // 17, b // 17)) + else: + candidates.append("#%02x%02x%02x" % (r, g, b)) + else: + # Can't use hex notation for RGBA + if compress: + sp = '' + else: + sp = ' ' + candidates.append("rgba(%d,%s%d,%s%d,%s%.6g)" % (r, sp, g, sp, b, sp, a)) + + if compress: + return min(candidates, key=len) + else: + return candidates[0] + + +# TODO be unicode-clean and delete this nonsense +DEFAULT_STRING_ENCODING = "utf8" + + +class String(Value): + """Represents both CSS quoted string values and CSS identifiers (such as + `left`). + + Makes no distinction between single and double quotes, except that the same + quotes are preserved on string literals that pass through unmodified. + Otherwise, double quotes are used. + """ + + sass_type_name = 'string' + + bad_identifier_rx = re.compile('[^-_a-zA-Z\x80-\U0010FFFF]') + + def __init__(self, value, quotes='"', literal=False): + if isinstance(value, String): + # TODO unclear if this should be here, but many functions rely on + # it + value = value.value + elif isinstance(value, Number): + # TODO this may only be necessary in the case of __radd__ and + # number values + value = six.text_type(value) + + if isinstance(value, six.binary_type): + warn(FutureWarning( + "String got a bytes type {0!r} " + "-- this will no longer be supported in pyScss 2.0" + .format(value) + )) + value = value.decode(DEFAULT_STRING_ENCODING) + + if not isinstance(value, six.text_type): + raise TypeError("Expected string, got {0!r}".format(value)) + + self.value = value + self.quotes = quotes + # TODO this isn't quite used yet + if literal: + self.original_literal = value + else: + self.original_literal = None + + @classmethod + def unquoted(cls, value, literal=False): + """Helper to create a string with no quotes.""" + return cls(value, quotes=None, literal=literal) + + def __hash__(self): + return hash(self.value) + + def __repr__(self): + if self.quotes: + quotes = '(' + self.quotes + ')' + else: + quotes = '' + return "<{0}{1} {2!r}>".format( + type(self).__name__, quotes, self.value) + + def __eq__(self, other): + return Boolean(isinstance(other, String) and self.value == other.value) + + def __add__(self, other): + if isinstance(other, String): + other_value = other.value + else: + other_value = other.render() + + return String( + self.value + other_value, + quotes='"' if self.quotes else None) + + def __mul__(self, other): + # DEVIATION: Ruby Sass doesn't do this, because Ruby doesn't. But + # Python does, and in Ruby Sass it's just fatal anyway. + if not isinstance(other, Number): + return super(String, self).__mul__(other) + + if not other.is_unitless: + raise TypeError("Can only multiply strings by unitless numbers") + + n = other.value + if n != int(n): + raise ValueError("Can only multiply strings by integers") + + return String(self.value * int(other.value), quotes=self.quotes) + + def _escape_character(self, match): + """Given a single character, return it appropriately CSS-escaped.""" + # TODO is there any case where we'd want to use unicode escaping? + # TODO unsure if this works with newlines + return '\\' + match.group(0) + + def _is_name_start(self, ch): + if ch == '_': + return True + if ord(ch) >= 128: + return True + if ch in string.ascii_letters: + return True + return False + + def render(self, compress=False): + # TODO should preserve original literals here too -- even the quotes. + # or at least that's what sass does. + # Escape and add quotes as appropriate. + if self.quotes is None: + # If you deliberately construct a bareword with bogus CSS in it, + # you're assumed to know what you're doing + return self.value + else: + return self._render_quoted() + + def render_interpolated(self, compress=False): + # Always render without quotes + return self.value + + def _render_bareword(self): + # TODO this is currently unused, and only implemented due to an + # oversight, but would make for a much better implementation of + # escape() + + # This is a bareword, so almost anything outside \w needs escaping + ret = self.value + ret = self.bad_identifier_rx.sub(self._escape_character, ret) + + # Also apply some minor quibbling rules about how barewords can + # start: with a "name start", an escape, a hyphen followed by one + # of those, or two hyphens. + if not ret: + # TODO is an unquoted empty string allowed to be rendered? + pass + elif ret[0] == '-': + if ret[1] in '-\\' or self._is_name_start(ret[1]): + pass + else: + # Escape the second character + # TODO what if it's a digit, oops + ret = ret[0] + '\\' + ret[1:] + elif ret[0] == '\\' or self._is_name_start(ret[0]): + pass + else: + # Escape the first character + # TODO what if it's a digit, oops + ret = '\\' + ret + + return ret + + def _render_quoted(self): + # Strictly speaking, the only things we need to quote are the quotes + # themselves, backslashes, and newlines. + # TODO Ruby Sass takes backslashes in barewords literally, but treats + # backslashes in quoted strings as escapes -- their mistake? + # TODO In Ruby Sass, generated strings never have single quotes -- but + # neither do variable interpolations, so I'm not sure what they're + # doing + quote = self.quotes + + ret = self.value + ret = ret.replace('\\', '\\\\') + ret = ret.replace(quote, '\\' + quote) + # Note that a literal newline is ignored when escaped, so we have to + # use the codepoint instead. But we'll leave the newline as well, to + # aid readability. + ret = ret.replace('\n', '\\a\\\n') + return quote + ret + quote + + +# TODO this needs to pretend the url(...) is part of the string for all string +# operations -- even the quotes! alas. +# TODO recasting a function to a String will lose the function part? whoops. +# maybe .value should just be, uh, the literal value instead of the insides??? +class Function(String): + """Function call pseudo-type, which crops up frequently in CSS as a string + marker. Acts mostly like a string, but has a function name and parentheses + around it. + """ + def __init__(self, string, function_name, quotes='"', literal=False): + super(Function, self).__init__(string, quotes=quotes, literal=literal) + self.function_name = function_name + + def render(self, compress=False): + return "{0}({1})".format( + self.function_name, + super(Function, self).render(compress), + ) + + def render_interpolated(self, compress=False): + return "{0}({1})".format( + self.function_name, + super(Function, self).render_interpolated(compress), + ) + + +class Url(Function): + # Bare URLs may not contain quotes, parentheses, or unprintables. Quoted + # URLs may, of course, contain whatever they like. + # Ref: http://dev.w3.org/csswg/css-syntax-3/#consume-a-url-token0 + bad_identifier_rx = re.compile("[$'\"()\\x00-\\x08\\x0b\\x0e-\\x1f\\x7f]") + + def __init__(self, string, **kwargs): + super(Url, self).__init__(string, 'url', **kwargs) + + def render(self, compress=False): + if self.quotes is None: + return self.render_interpolated(compress) + else: + inside = self._render_quoted() + return "url(" + inside + ")" + + def render_interpolated(self, compress=False): + # Always render without quotes. + # When doing that, we need to escape some stuff to make sure the result + # is valid CSS. + inside = self.bad_identifier_rx.sub( + self._escape_character, self.value) + + return "url(" + inside + ")" + + +class Map(Value): + sass_type_name = 'map' + + def __init__(self, pairs, index=None): + self.pairs = tuple(pairs) + + if index is None: + self.index = {} + for key, value in pairs: + self.index[key] = value + else: + self.index = index + + def __repr__(self): + return "" % (", ".join("%s: %s" % pair for pair in self.pairs),) + + def __hash__(self): + return hash(self.pairs) + + def __len__(self): + return len(self.pairs) + + def __iter__(self): + return iter(self.pairs) + + def __getitem__(self, index): + return List(self.pairs[index], use_comma=True) + + def __eq__(self, other): + try: + return self.pairs == other.to_pairs() + except ValueError: + return NotImplemented + + def to_dict(self): + return self.index + + def to_pairs(self): + return self.pairs + + def render(self, compress=False): + raise TypeError("Cannot render map %r as CSS" % (self,)) + + +def expect_type(value, types, unit=any): + if not isinstance(value, types): + if isinstance(types, type): + types = (type,) + sass_type_names = list(set(t.sass_type_name for t in types)) + sass_type_names.sort() + + # Join with commas in English fashion + if len(sass_type_names) == 1: + sass_type = sass_type_names[0] + elif len(sass_type_names) == 2: + sass_type = ' or '.join(sass_type_names) + else: + sass_type = ', '.join(sass_type_names[:-1]) + sass_type += ', or ' + sass_type_names[-1] + + raise TypeError("Expected %s, got %r" % (sass_type, value)) + + if unit is not any and isinstance(value, Number): + if unit is None and not value.is_unitless: + raise ValueError("Expected unitless number, got %r" % (value,)) + + elif unit == '%' and not ( + value.is_unitless or value.is_simple_unit('%')): + raise ValueError("Expected unitless number or percentage, got %r" % (value,)) diff --git a/aip_site/vendor/pyscss/scss/util.py b/aip_site/vendor/pyscss/scss/util.py new file mode 100644 index 0000000..c229438 --- /dev/null +++ b/aip_site/vendor/pyscss/scss/util.py @@ -0,0 +1,248 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import base64 +import hashlib +import os +import re +import sys +import time +from functools import wraps + +import six + +from scss import config + +BASE_DIR = os.path.dirname(__file__) + + +def split_params(params): + params = params.split(',') or [] + if params: + final_params = [] + param = params.pop(0) + try: + while True: + while param.count('(') != param.count(')'): + try: + param = param + ',' + params.pop(0) + except IndexError: + break + final_params.append(param) + param = params.pop(0) + except IndexError: + pass + params = final_params + return params + + +def dequote(s): + if s and s[0] in ('"', "'") and s[-1] == s[0]: + s = s[1:-1] + s = unescape(s) + return s + + +def depar(s): + while s and s[0] == '(' and s[-1] == ')': + s = s[1:-1] + return s + + +def to_str(num): + try: + render = num.render + except AttributeError: + pass + else: + return render() + + if isinstance(num, dict): + s = sorted(num.items()) + sp = num.get('_', '') + return (sp + ' ').join(to_str(v) for n, v in s if n != '_') + elif isinstance(num, float): + num = ('%0.05f' % round(num, 5)).rstrip('0').rstrip('.') + return num + elif isinstance(num, bool): + return 'true' if num else 'false' + elif num is None: + return '' + return six.text_type(num) + + +def to_float(num): + if isinstance(num, (float, int)): + return float(num) + num = to_str(num) + if num and num[-1] == '%': + return float(num[:-1]) / 100.0 + else: + return float(num) + + +def escape(s): + return re.sub(r'''(["'])''', r'\\\1', s) # do not escape '\' + + +# Deprecated; use the unescape() from cssdefs instead +def unescape(s): + return re.sub(r'''\\(['"\\])''', r'\1', s) # do unescape '\' + + +def normalize_var(var): + """Sass defines `foo_bar` and `foo-bar` as being identical, both in + variable names and functions/mixins. This normalizes everything to use + dashes. + """ + return var.replace('_', '-') + + +def make_data_url(mime_type, data): + """Generate a `data:` URL from the given data and MIME type.""" + return "data:{0};base64,{1}".format( + mime_type, base64.b64encode(data).decode('ascii')) + + +def make_filename_hash(key): + """Convert the given key (a simple Python object) to a unique-ish hash + suitable for a filename. + """ + key_repr = repr(key).replace(BASE_DIR, '').encode('utf8') + # This is really stupid but necessary for making the repr()s be the same on + # Python 2 and 3 and thus allowing the test suite to run on both. + # TODO better solutions include: not using a repr, not embedding hashes in + # the expected test results + if sys.platform == 'win32': + # this is to make sure the hash is the same on win and unix platforms + key_repr = key_repr.replace(b'\\\\', b'/') + key_repr = re.sub(b"\\bu'", b"'", key_repr) + key_hash = hashlib.md5(key_repr).digest() + return base64.b64encode(key_hash, b'__').decode('ascii').rstrip('=') + + +################################################################################ +# Function timing decorator +profiling = {} + + +def print_timing(level=0): + def _print_timing(func): + if config.VERBOSITY: + def wrapper(*args, **kwargs): + if config.VERBOSITY >= level: + t1 = time.time() + res = func(*args, **kwargs) + t2 = time.time() + profiling.setdefault(func.func_name, 0) + profiling[func.func_name] += (t2 - t1) + return res + else: + return func(*args, **kwargs) + return wrapper + else: + return func + return _print_timing + + +################################################################################ +# Profiler decorator +def profile(fn): + import cProfile + import pstats + + @wraps(fn) + def wrapper(*args, **kwargs): + profiler = cProfile.Profile() + stream = six.StringIO() + profiler.enable() + try: + res = fn(*args, **kwargs) + finally: + profiler.disable() + stats = pstats.Stats(profiler, stream=stream) + stats.sort_stats('time') + print >>stream, "" + print >>stream, "=" * 100 + print >>stream, "Stats:" + stats.print_stats() + print >>stream, "=" * 100 + print >>stream, "Callers:" + stats.print_callers() + print >>stream, "=" * 100 + print >>stream, "Callees:" + stats.print_callees() + print >>sys.stderr, stream.getvalue() + stream.close() + return res + return wrapper + + +################################################################################ +# http://code.activestate.com/recipes/325905-memoize-decorator-with-timeout/ + +class tmemoize(object): + """ + Memoize With Timeout + + Usage: + @tmemoize() + def z(a,b): + return a + b + + @tmemoize(timeout=5) + def x(a,b): + return a + b + """ + _caches = {} + _timeouts = {} + _collected = time.time() + + def __init__(self, timeout=60, gc=3600): + self.timeout = timeout + self.gc = gc + + def collect(self): + """Clear cache of results which have timed out""" + for func in self._caches: + cache = {} + for key in self._caches[func]: + if (time.time() - self._caches[func][key][1]) < self._timeouts[func]: + cache[key] = self._caches[func][key] + self._caches[func] = cache + + def __call__(self, func): + self._caches[func] = {} + self._timeouts[func] = self.timeout + + @wraps(func) + def wrapper(*args): + key = args + now = time.time() + cache = self._caches[func] + try: + ret, last = cache[key] + if now - last > self.timeout: + raise KeyError + except KeyError: + ret, last = cache[key] = (func(*args), now) + if now - self._collected > self.gc: + self.collect() + self._collected = time.time() + return ret + return wrapper + + +################################################################################ +# Memoized getmtime (can accept storage) +@tmemoize() +def getmtime(filename, storage=None): + try: + if storage: + d_obj = storage.modified_time(filename) + return int(time.mktime(d_obj.timetuple())) + else: + return int(os.path.getmtime(filename)) + except: + pass diff --git a/setup.py b/setup.py index 44e7860..5bdc05b 100644 --- a/setup.py +++ b/setup.py @@ -52,7 +52,6 @@ 'markupsafe==2.1.1', 'pygments==2.13.0', 'pymdown-extensions==9.7', - 'pyscss==1.4.0', 'pyyaml==6.0.1', 'six==1.16.0', 'types-Markdown==3.4.2.1',