summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2014-08-28 17:03:16 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2014-09-01 21:30:56 -0700
commit80c8b5e61dac86efe686f924d8670cc6fed21575 (patch)
tree8189771a8d6da681dfec43725ec16866429eb79c
parent3c96edaa71ace1d28ba930c251f7880b0edf5167 (diff)
downloadpyscss-80c8b5e61dac86efe686f924d8670cc6fed21575.tar.gz
WIP: Taking a crack at separating block parsing from evaluation.
-rw-r--r--scss/blockast.py234
-rw-r--r--scss/compiler.py176
-rw-r--r--scss/expression.py1
3 files changed, 409 insertions, 2 deletions
diff --git a/scss/blockast.py b/scss/blockast.py
new file mode 100644
index 0000000..153f7aa
--- /dev/null
+++ b/scss/blockast.py
@@ -0,0 +1,234 @@
+from __future__ import absolute_import
+from __future__ import print_function
+from __future__ import unicode_literals
+from __future__ import division
+
+import logging
+
+from scss.expression import Calculator
+from scss.types import List
+from scss.types import Null
+from scss.util import normalize_var # TODO put in... namespace maybe?
+
+
+log = logging.getLogger(__name__)
+
+
+# TODO currently none of these know where they come from; whoops?
+
+class Node(object):
+ is_block = False
+ """Whether this node is a block, i.e. has children."""
+
+ def __repr__(self):
+ return "<{0} {1!r}>".format(
+ type(self).__name__,
+ repr(self.__dict__)[:100],
+ )
+
+ def add_child(self, node):
+ raise RuntimeError("Only blocks can have children")
+
+ def evaluate(self, namespace):
+ raise NotImplementedError
+
+
+class Declaration(Node):
+ pass
+
+class Assignment(Declaration):
+ def __init__(self, name, value):
+ self.name = name
+ self.value = value
+
+class CSSDeclaration(Declaration):
+ def __init__(self, prop, value_expression):
+ self.prop = prop
+ self.value_expression = value_expression
+
+ @classmethod
+ def parse(cls, prop, value):
+ # TODO prop needs parsing too, but interp-only!
+
+ # TODO this is a bit naughty, but uses no state except the ast_cache --
+ # which should maybe be in the Compiler anyway...?
+ value_expression = Calculator().parse_expression(value)
+
+ return cls(prop, value_expression)
+
+ def evaluate(self, compilation):
+ prop = self.prop
+ value = self.value_expression.evaluate(compilation.current_calculator)
+ # TODO this is where source maps would need to get their info
+ compilation.add_declaration(prop, value)
+
+
+class _AtRuleMixin(object):
+
+ is_atrule = True
+
+ directive = None
+ """The name of the at-rule, including the @."""
+
+ argument = None
+ """Any text between the at-rule and the opening brace."""
+ # TODO should this be parsed? can't be for unknown rules...
+
+ def __init__(self, directive, argument):
+ super(_AtRuleMixin, self).__init__()
+ self.directive = directive
+ self.argument = argument
+
+ 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
+
+ def evaluate(*args):
+ log.warn("Not yet implemented")
+
+class AtRule(_AtRuleMixin, Node):
+ """An at-rule with no children, e.g. ``@import``."""
+
+
+class Block(Node):
+ """Base class for a block -- an arbitrary header followed by a list of
+ children delimited with curly braces.
+ """
+ is_block = True
+
+ def __init__(self):
+ self.children = []
+
+ def add_child(self, node):
+ self.children.append(node)
+
+
+class SelectorBlock(Block):
+ """A regular CSS-like block, with selectors for the header and a list of
+ zero or more declarations. This is the most likely kind of node to end up
+ as CSS output.
+ """
+ is_selector = True
+
+ def __init__(self, selector_string):
+ super(SelectorBlock, self).__init__()
+ # NOTE: This can't really be parsed as a selector yet, since it might
+ # have interpolations that alter the selector parsing.
+ # TODO eager-parse if there's no #{ in it?
+ self.selector_string = selector_string
+
+ def __repr__(self):
+ return "<%s %r>" % (type(self).__name__, self.selector_string)
+
+ def evaluate(self, compilation):
+ # TODO parse earlier; need a rule that ONLY looks for interps
+ plain_selector_string = compilation.current_calculator.do_glob_math(self.selector_string)
+ from .selector import Selector
+ selector = Selector.parse_many(plain_selector_string)
+ with compilation.nest_selector(selector):
+ # TODO un-recurse
+ for child in self.children:
+ child.evaluate(compilation)
+
+
+class AtRuleBlock(_AtRuleMixin, Block):
+ """An at-rule followed by a block, e.g. ``@media``."""
+
+
+class ScopeBlock(Block):
+ """A block that looks like ``background: { color: white; }``. This is
+ Sass shorthand for setting multiple properties with a common prefix.
+ """
+ is_scope = True
+
+ def __init__(self, scope, unscoped_value):
+ super(ScopeBlock, self).__init__()
+ self.scope = scope
+
+ if unscoped_value:
+ self.unscoped_value = unscoped_value
+ else:
+ self.unscoped_value = None
+
+ def add_child(self, node):
+ # TODO pretty sure you can't, say, nest a scope block inside a scope
+ # block
+ super(ScopeBlock, self).add_child(node)
+
+
+class FileBlock(Block):
+ """Special block representing the entire contents of a file. ONLY has
+ children; there's no header.
+ """
+
+ # TODO can only contain blocks
+
+ def evaluate(self, compilation):
+ for child in self.children:
+ child.evaluate(compilation)
+
+
+class AtEachBlock(AtRuleBlock):
+ directive = '@each'
+
+ def __init__(self, variable_names, expression, unpack):
+ # TODO fix parent to not assign to directive/argument and use super
+ # here
+ Block.__init__(self)
+
+ self.variable_names = variable_names
+ self.expression = expression
+ self.unpack = unpack
+
+ @classmethod
+ def parse(cls, argument):
+ # TODO this is flaky; need a real grammar rule here
+ varstring, _, valuestring = argument.partition(' in ')
+ # TODO this is a bit naughty, but uses no state except the ast_cache --
+ # which should maybe be in the Compiler anyway...?
+ expression = Calculator().parse_expression(valuestring)
+
+ variable_names = [
+ # TODO broke support for #{} inside the var name
+ normalize_var(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(variable_names) > 1
+ if not variable_names[-1]:
+ variable_names.pop()
+
+ return cls(variable_names, expression, unpack)
+
+ def evaluate(self, compilation):
+ # TODO with compilation.new_scope() as namespace:
+ namespace = compilation.current_namespace
+
+ # TODO compilation.calculator? or change calculator to namespace?
+ calc = Calculator(namespace)
+ values = self.expression.evaluate(calc)
+ # TODO is the from_maybe necessary here? doesn't Value do __iter__?
+ for values in List.from_maybe(values):
+ if self.unpack:
+ values = List.from_maybe(values)
+ for i, variable_name in enumerate(self.variable_names):
+ if i >= len(values):
+ value = Null()
+ else:
+ value = values[i]
+ namespace.set_variable(variable_name, value)
+ else:
+ namespace.set_variable(self.variable_names[0], values)
+
+ # TODO i would love to get this recursion out of here -- clever use
+ # of yield?
+ for child in self.children:
+ child.evaluate(compilation)
+
diff --git a/scss/compiler.py b/scss/compiler.py
index b6d8851..8fceed1 100644
--- a/scss/compiler.py
+++ b/scss/compiler.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
from __future__ import division
from collections import defaultdict
+from contextlib import contextmanager
from enum import Enum
import glob
from itertools import product
@@ -51,6 +52,7 @@ from scss.types import Undefined
from scss.types import Url
from scss.util import normalize_var # TODO put in... namespace maybe?
+from .blockast import *
# TODO should mention logging for the programmatic interface in the
# documentation
@@ -335,6 +337,32 @@ class Compilation(object):
self.rules.append(rule)
children.append(rule)
+ # TODO would be neat to cache the ASTs for files in the compiler
+ file_block = self.parse_blocks(source_file.contents)
+
+ # The compilation doubles as the evaluation context, keeping track of
+ # running state while evaluating the AST here
+ # TODO maybe stateless stuff, like parsing, should go exclusively on
+ # the compiler then?
+ # TODO in which case maybe evaluation should go here and Calculator
+ # should vanish? (but then it's awkward to use programmatically, hmm)
+ self._ancestry_stack = [RuleAncestry()]
+ self.current_namespace = root_namespace
+ self.current_calculator = self._make_calculator(self.current_namespace)
+ self.declarations = []
+
+ file_block.evaluate(self)
+
+ for ancestry, properties in self.declarations:
+ for header in ancestry.headers:
+ print(header.render(), end=' ')
+ print("{")
+ for prop, value in properties:
+ print(" {0}: {1};".format(prop, value.render()))
+ print("}")
+ return
+
+
for rule in children:
self.manage_children(rule, scope)
@@ -355,6 +383,127 @@ class Compilation(object):
undefined_variables_fatal=self.compiler.undefined_variables_fatal,
)
+ def parse_blocks(self, string, scope=''):
+ # TODO eliminate scope by making it a block attribute
+ from collections import deque
+ remaining = deque()
+
+ # TODO i guess root should be a node too? what kind?
+ root = FileBlock()
+
+ remaining.append((root, string))
+ while remaining:
+ parent, string = remaining.popleft()
+ for lineno, header_string, child_string in locate_blocks(string):
+ if child_string is None:
+ # No actual block here; either a simple declaration (like
+ # @import) or a property assignment
+ pass
+ # TODO include source in these
+ # TODO classes, obv.
+
+ is_block = child_string is not None
+ node = self.parse_header(parent, header_string, is_block)
+ parent.add_child(node)
+ if is_block:
+ remaining.append((node, child_string))
+
+ print(lineno, repr(header_string), repr(node))
+ from pprint import pprint
+ print()
+ pprint(root)
+
+ return root
+
+ def parse_header(self, parent_block, header_string, is_block):
+ """Given a line number, header string, and whether or not this is a
+ block as opposed to a declaration (roughly the output of
+ `locate_blocks`), return an appropriate AST node. Note that this does
+ NOT actually parse the children; it returns an empty container.
+ """
+ # TODO how do the rest of the properties like source_file get in here?
+ # just pass in the parent, maybe, and let each of the AST classes
+ # figure out what to do at runtime? i do kinda like that.
+ header_string = header_string.strip()
+
+ # Simple pre-processing
+ if header_string.startswith('+') and not is_block:
+ # 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
+ # TODO: warn_deprecated
+ header_string = '@include ' + header_string[1:]
+ try:
+ if '(' not in header_string or header_string.index(':') < header_string.index('('):
+ header_string = header_string.replace(':', '(', 1)
+ if '(' in header_string:
+ header_string += ')'
+ except ValueError:
+ pass
+ elif header_string.startswith('='):
+ # Expand '=' at the beginning of a rule as @mixin
+ header_string = '@mixin ' + header_string[1:]
+ elif header_string.startswith('@prototype '):
+ # Remove '@prototype '
+ # TODO what is @prototype??
+ # TODO warn_deprecated(...)
+ header_string = header_string[11:]
+
+ # Now we decide what kind of thing we actually have.
+ if header_string.startswith('@'):
+ if header_string.lower().startswith('@else if '):
+ directive = '@else if'
+ argument = header_string[9:]
+ else:
+ chunks = header_string.split(None, 1)
+ if len(chunks) == 2:
+ directive, argument = chunks
+ else:
+ directive, argument = header_string, None
+ directive = directive.lower()
+
+ if is_block:
+ # TODO need some real registration+dispatch here
+ if directive == '@each':
+ return AtEachBlock.parse(argument)
+ else:
+ return AtRuleBlock(directive, argument)
+ else:
+ return AtRule(directive, argument)
+
+ # Try splitting this into "name : value"
+ # TODO strictly speaking this isn't right -- consider foo#{":"}bar.
+ # the right fix is probably to shift this into the grammar as its own
+ # goal rule... but that doesn't work in a selector, argh!
+ name, colon, value = header_string.partition(':')
+ if colon:
+ name = name.rstrip()
+ value = value.lstrip()
+ if is_block:
+ # This is a "scope" block. Syntax is `<scope>: [value]` -- if
+ # the optional value exists, it becomes the first declaration,
+ # with no suffix.
+ scope = name
+ unscoped_value = value
+ # TODO parse value
+ return ScopeBlock(scope, unscoped_value)
+ elif name.startswith('$'):
+ # Sass variable assignment
+ # TODO parse value, pull off !default and !global
+ return Assignment(name, value)
+ else:
+ # Regular old CSS property declaration
+ return CSSDeclaration.parse(name, value)
+
+ # If it's nothing else special, it must be a plain old selector block
+ # TODO need the xcss parsing here
+ if is_block:
+ return SelectorBlock(header_string)
+
+ # TODO this should (a) not be a syntax error, (b) get the right stack
+ # trace
+ raise SyntaxError("Couldn't figure out what kind of block this is")
+
# @print_timing(4)
def manage_children(self, rule, scope):
try:
@@ -1052,8 +1201,9 @@ class Compilation(object):
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
+ 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)
# @print_timing(10)
@@ -1437,6 +1587,28 @@ class Compilation(object):
return rules_by_file, css_files
+ # State-munging helpers
+
+ @property
+ def current_ancestry(self):
+ return self._ancestry_stack[-1]
+
+ @contextmanager
+ def nest_selector(self, selector):
+ new_ancestry = self._ancestry_stack[-1].with_nested_selectors(selector)
+ self._ancestry_stack.append(new_ancestry)
+ try:
+ yield new_ancestry
+ finally:
+ self._ancestry_stack.pop()
+
+ def add_declaration(self, prop, value):
+ if self.declarations and self.declarations[-1][0] == self.current_ancestry:
+ self.declarations[-1][1].append((prop, value))
+ else:
+ self.declarations.append((self.current_ancestry, [(prop, value)]))
+
+
# @print_timing(3)
def create_css(self, rules):
"""
diff --git a/scss/expression.py b/scss/expression.py
index 39eb623..a51fef4 100644
--- a/scss/expression.py
+++ b/scss/expression.py
@@ -181,4 +181,5 @@ class Calculator(object):
return Literal(String.unquoted(string))
return self.parse_expression(string, 'goal_interpolated_anything')
+
__all__ = ('Calculator',)