import logging
from collections import deque
from .exceptions import ParseError
from .lexer import Lexer, Token
from .nodes.arguments import Arguments
from .nodes.atblock import Atblock
from .nodes.atrule import Atrule
from .nodes.binop import BinOp
from .nodes.block import Block
from .nodes.boolean import true
from .nodes.call import Call
from .nodes.charset import Charset
from .nodes.each import Each
from .nodes.expression import Expression
from .nodes.extend import Extend
from .nodes.feature import Feature
from .nodes.function import Function
from .nodes.group import Group
from .nodes.ident import Ident
from .nodes.ifnode import If
from .nodes.import_node import Import
from .nodes.keyframes import Keyframes
from .nodes.literal import Literal
from .nodes.media import Media
from .nodes.member import Member
from .nodes.namespace import Namespace
from .nodes.node import Node
from .nodes.nothing import Nothing
from .nodes.null import null
from .nodes.object_node import ObjectNode
from .nodes.params import Params
from .nodes.property import Property
from .nodes.query import Query
from .nodes.query_list import QueryList
from .nodes.return_node import ReturnNode
from .nodes.root import Root
from .nodes.selector import Selector
from .nodes.supports import Supports
from .nodes.ternary import Ternary
from .nodes.unaryop import UnaryOp
from .units import units
log = logging.getLogger(__name__)
[docs]class Parser:
[docs] def __init__(self, s, options: dict):
self.cond = None
self.s = s
self.options = options
self.lexer = Lexer(s, options)
self.prefix = options.get("prefix", "")
self.root = options.get("root", Root())
self.state = ["root"]
self.stash = []
self.parens = 0
self.css = 0
self.parent = None
self.selector_scope = None
self.bracketed = None
self.in_property = None
self._ident = None
self.operand = None
self.allow_postfix = None
self.prev_state = None
self.lineno = 1
self.column = 1
#
# Selector composite tokens.
#
selector_tokens = [
"ident",
"string",
"selector",
"function",
"comment",
"boolean",
"space",
"color",
"unit",
"for",
"in",
"[",
"]",
"(",
")",
"+",
"-",
"*",
"*=",
"<",
">",
"=",
":",
"&",
"&&",
"~",
"{",
"}",
".",
"..",
"/",
]
#
# CSS pseudo-classes and pseudo-elements.
# See http://dev.w3.org/csswg/selectors4/
#
pseudo_selectors = [
# Logical Combinations
"matches",
"not",
# Linguistic Pseudo-classes
"dir",
"lang",
# Location Pseudo-classes
"any-link",
"link",
"visited",
"local-link",
"target",
"scope",
# User Action Pseudo-classes
"hover",
"active",
"focus",
"drop",
# Time-dimensional Pseudo-classes
"current",
"past",
"future",
# The Input Pseudo-classes
"enabled",
"disabled",
"read-only",
"read-write",
"placeholder-shown",
"checked",
"indeterminate",
"valid",
"invalid",
"in-range",
"out-of-range",
"required",
"optional",
"user-error",
# Tree-Structural pseudo-classes
"root",
"empty",
"blank",
"nth-child",
"nth-last-child",
"first-child",
"last-child",
"only-child",
"nth-of-type",
"nth-last-of-type",
"first-of-type",
"last-of-type",
"only-of-type",
"nth-match",
"nth-last-match",
# Grid-Structural Selectors
"nth-column",
"nth-last-column",
# Pseudo-elements
"first-line",
"first-letter",
"before",
"after",
# Non-standard
"selection",
]
def current_state(self):
return self.state[-1]
def previous_state(self):
try:
return self.state[-2]
except Exception as e:
# todo: handle this properly
raise e
[docs] def parse(self) -> Node:
"""
Parse the given source string.
:return: The root Node.
:rtype: Node
"""
block = self.parent = self.root
while "eos" != self.peek().type:
self.skip_whitespace()
if "eos" == self.peek().type:
break
stmt = self.statement()
self.accept(";")
if stmt is None:
self.error(
"unexpected token {peek}, not " "allowed at the root level"
)
block.append(stmt)
return block
def error(self, message: str):
t = self.peek().type
value = ""
if self.peek().value:
value = str(self.peek())
if value.strip() == t.strip():
value = ""
raise ParseError(message.format(peek='"{}{}"'.format(t, value)))
def accept(self, types):
if isinstance(types, str):
if self.peek().type == types:
return self.next()
elif isinstance(types, list):
if self.peek().type in types:
return self.next()
return None
def expect(self, type):
if type != self.peek().type:
self.error(f'expected "{type}", got {{peek}}')
return self.next()
def next(self) -> Token:
if self.stash:
tok = self.stash.pop()
else:
tok = self.lexer.next()
if tok.value and isinstance(tok.value, Node):
tok.value.lineno = tok.lineno
tok.value.column = tok.column
self.lineno = tok.lineno
self.column = tok.column
log.debug(f"Returning {tok}.")
return tok
def __iter__(self):
return self
def __next__(self):
tok = self.next()
if tok.type == "eos":
raise StopIteration
return tok
def peek(self) -> Token:
return self.lexer.peek()
def lookahead(self, n):
return self.lexer.lookahead(n)
[docs] def is_selector_token(self, n: int) -> bool:
"""
Check if the token at n is a valid selector token.
:param n:
:return:
"""
la = self.lookahead(n).type
if la == "for":
return self.bracketed
elif la == "[":
self.bracketed = True
return True
elif la == "]":
self.bracketed = False
return True
else:
return la in self.selector_tokens
def is_pseudo_selector(self, n):
value = self.lookahead(n).value
if value and hasattr(value, "name"):
return value.name in self.pseudo_selectors
return False
def line_contains(self, type):
i = 1
la = self.lookahead(i)
while True:
if la.type in ["indent", "outdent", "newline", "eos"]:
return False
if la.type == type:
return True
i += 1
la = self.lookahead(i)
[docs] def selector_token(self):
"""Valid selector tokens."""
if self.is_selector_token(1):
if "{" == self.peek().type:
if not self.line_contains("}"):
# unclosed - must be a block
return None
# check if ':' is within the braces.
# though not required by Stylus, chances
# are if someone is using {} they will
# use CSS-style props, helping us with
# the ambiguity in this case
i = 1
la = self.lookahead(i)
while la:
if la.type == "}":
# check empty block
if i == 2 or (
i == 3 and self.lookahead(i - 1).type == "space"
):
return None
break
if la.type == ":":
return None
i += 1
la = self.lookahead(i)
return self.next()
return None
def skip(self, tokens):
while self.peek().type in tokens:
self.next()
def skip_whitespace(self):
self.skip(["space", "indent", "outdent", "newline"])
def skip_spaces(self):
self.skip(["space"])
def skip_spaces_and_comments(self):
self.skip(["space", "comment"])
def looks_like_function_definition(self, i):
return self.lookahead(i).type in ["indent", "{"]
def looks_like_selector(self, from_property=False): # noqa: C901
i = 1
brace = None
# real property
if (
from_property
and ":" == self.lookahead(i + 1).type
and (
self.lookahead(i + 1).space
or "indent" == self.lookahead(i + 2).type
)
):
return False
# assume selector when an ident is followed by a selector
while "ident" == self.lookahead(i).type and (
"newline" == self.lookahead(i + 1).type
or "," == self.lookahead(i + 1).type
):
i += 2
while self.is_selector_token(i) or "," == self.lookahead(i).type:
if "selector" == self.lookahead(i).type:
return True
if "&" == self.lookahead(i + 1).type:
return True
if (
"." == self.lookahead(i).type
and "ident" == self.lookahead(i + 1).type
):
return True
if (
"*" == self.lookahead(i).type
and "newline" == self.lookahead(i + 1).type
):
return True
# pseudo-elements
if (
":" == self.lookahead(i).type
and ":" == self.lookahead(i + 1).type
):
return true
# #a after an ident and newline
if (
"color" == self.lookahead(i).type
and "newline" == self.lookahead(i - 1).type
):
return true
if self.looks_like_attribute_selector(i):
return True
if (
"=" == self.lookahead(i).type
or "function" == self.lookahead(i).type
) and "{" == self.lookahead(i + 1).type:
return False
# hash values inside properties
if (
":" == self.lookahead(i).type
and not self.is_pseudo_selector(i + 1)
and self.line_contains(".")
):
return False
# the ':' token within braces signifies
# a selector. ex: "foo{bar:'baz'}"
if "{" == self.lookahead(i).type:
brace = True
elif "}" == self.lookahead(i).type:
brace = False
if brace and ":" == self.lookahead(i).type:
return True
# '{' preceded by a space is considered a selector.
# for example "foo{bar}{baz}" may be a property,
# however "foo{bar} {baz}" is a selector
if (
"space" == self.lookahead(i).type
and "{" == self.lookahead(i + 1).type
):
return True
# assume pseudo selectors are NOT properties
# as 'td:th-child(1)' may look like a property
# and function call to the parser otherwise
i += 1
if (
":" == self.lookahead(i - 1).type
and not self.lookahead(i - 1).space
and self.is_pseudo_selector(i)
):
return True
# trailing space
if (
"space" == self.lookahead(i).type
and "newline" == self.lookahead(i + 1).type
and "{" == self.lookahead(i + 2).type
):
return True
if (
"," == self.lookahead(i).type
and "newline" == self.lookahead(i + 1).type
):
return True
# trailing comma
if (
"," == self.lookahead(i).type
and "newline" == self.lookahead(i + 1).type
):
return True
# trailing brace
if (
"{" == self.lookahead(i).type
and "newline" == self.lookahead(i + 1).type
):
return True
# css-style mode, false on ; }
if self.css > 0 and (
";" == self.lookahead(i).type or "}" == self.lookahead(i - 1).type
):
return False
# trailing separators
while self.lookahead(i).type not in [
"indent",
"outdent",
"newline",
"for",
"if",
";",
"}",
"eos",
]:
i += 1
if "indent" == self.lookahead(i).type:
return True
return False
def looks_like_attribute_selector(self, n):
type = self.lookahead(n).type
if "=" == type and self.bracketed:
return True
return (
("ident" == type or "string" == type)
and "]" == self.lookahead(n + 1).type
and (
"newline" == self.lookahead(n + 2).type
or self.is_selector_token(n + 2)
)
and not self.line_contains(":")
and not self.line_contains("=")
)
def looks_like_keyframe(self):
i = 2
if self.lookahead(i).type in ["{", "indent", ","]:
return True
elif self.lookahead(i).type == "newline":
i += 1
while self.lookahead(i).type in ["unit", "newline"]:
i += 1
type = self.lookahead(i).type
return "indent" == type or "{" == type
def state_allows_selector(self):
if self.current_state() in [
"root",
"atblock",
"selector",
"conditional",
"function",
"atrule",
"for",
]:
return True
def assign_atblock(self, expr):
try:
expr.append(self.atblock(expr))
except Exception:
self.error(
"invalid right-hand side operand in assignment, got {peek}"
)
def statement(self):
stmt = self.stmt()
state = self.prev_state
# special-case statements since it
# is not an expression. We could
# implement postfix conditionals at
# the expression level, however they
# would then fail to enclose properties
if self.allow_postfix:
self.allow_postfix = False
state = "expression"
if state in ["assignment", "expression", "function arguments"]:
while True:
op = self.accept(["if", "unless", "for"])
if op and op.type in ["if", "unless"]:
stmt = If(
self.expression(),
stmt,
lineno=self.lineno,
column=self.column,
)
stmt.postfix = true
stmt.negate = "unless" == op.type
self.accept(";")
elif op and op.type == "for":
val = self.id().name
key = None
if self.accept(","):
key = self.id().name
self.expect("in")
each = Each(val, key, self.expression())
block = Block(
self.parent,
each,
lineno=self.lineno,
column=self.column,
)
block.append(stmt)
each.block = block
stmt = each
else:
break
return stmt
def stmt(self):
type = self.peek().type
if type == "keyframes":
return self.keyframes()
elif type == "-moz-document":
return self.mozdocument()
elif type in [
"comment",
"selector",
"literal",
"charset",
"namespace",
"import",
"require",
"extend",
"media",
"atrule",
"ident",
"scope",
"supports",
"unless",
"function",
"for",
"if",
]:
return self.__getattribute__(f"stmt_{type}")()
elif type in "return":
return self.return_expression()
elif type == "{":
return self.property()
else:
if self.state_allows_selector():
if type in [
"color",
"~",
">",
"<",
":",
"&",
"&&",
"[",
".",
"/",
]:
return self.stmt_selector()
elif type == "..":
# relative reference
if "/" == self.lookahead(2).type:
return self.stmt_selector()
elif type == "+":
if "function" == self.lookahead(2).type:
return self.function_call()
else:
return self.stmt_selector()
elif type == "*":
return self.property()
elif type == "unit":
if self.looks_like_keyframe():
return self.stmt_selector()
elif type == "-":
if "{" == self.lookahead(2).type:
return self.property()
expr = self.expression()
if expr.is_empty():
self.error("unexpected {peek}")
return expr
def stmt_comment(self):
node = self.next().value
self.skip_spaces()
return node
def stmt_literal(self):
return self.expect("literal").value
def stmt_charset(self):
self.expect("charset")
str = self.expect("string").value
self.allow_postfix = True
return Charset(str)
def stmt_namespace(self):
self.expect("namespace")
self.skip_spaces_and_comments()
prefix = self.accept("ident")
if prefix:
prefix = prefix.value
self.skip_spaces_and_comments()
str = self.accept("string")
if not str:
str = self.url()
self.allow_postfix = True
return Namespace(str, prefix)
def stmt_import(self):
self.expect("import")
self.allow_postfix = True
return Import(
self.expression(), False, lineno=self.lineno, column=self.column
)
def stmt_require(self):
self.expect("require")
self.allow_postfix = True
return Import(
self.expression(), True, lineno=self.lineno, column=self.column
)
def stmt_extend(self):
tok = self.expect("extend")
selectors = []
try:
while True:
arr = self.selector_parts()
if arr is None or len(arr) == 0:
if self.accept(","):
continue
else:
raise TypeError
sel = Selector(arr)
selectors.append(sel)
if self.peek().type != "!":
if self.accept(","):
continue
else:
raise TypeError
tok = self.lookahead(2)
if tok.type not in ["ident", "optional"]:
continue
self.skip(["!", "ident"])
sel.optional = True
token = self.accept(",")
if token is None:
break
except TypeError:
pass
node = Extend(selectors)
node.lineno = tok.lineno
node.column = tok.column
return node
def stmt_media(self):
self.expect("media")
self.state.append("atrule")
media = Media(self.queries())
media.block = self.block(media)
self.prev_state = self.state[-1]
self.state.pop()
return media
def queries(self):
queries = QueryList()
while True:
self.skip(["comment", "newline", "space"])
queries.append(self.query())
self.skip(["comment", "newline", "space"])
if not self.accept(","):
break
return queries
def query(self):
query = Query()
# hash values support
if self.peek().type == "ident" and (
self.lookahead(2).type == "." or self.lookahead(2).type == "["
):
self.cond = True
expr = self.expression()
self.cond = False
query.append(Feature(expr.nodes))
return query
pred = self.accept(["ident", "not"])
if pred:
pred = Literal(pred.value, lineno=self.lineno, column=self.lineno)
self.skip_spaces_and_comments()
id = self.accept("ident")
if id:
query.type = id.value
query.predicate = pred
else:
query.type = pred
self.skip_spaces_and_comments()
if self.accept("&&") is None:
return query
while True:
query.append(self.feature())
if self.accept("&&") is None:
break
return query
def feature(self):
self.skip_spaces_and_comments()
self.expect("(")
self.skip_spaces_and_comments()
node = Feature(self.interpolate())
self.skip_spaces_and_comments()
self.accept(":")
self.skip_spaces_and_comments()
self.in_property = True
node.expr = self.list()
self.in_property = False
self.skip_spaces_and_comments()
self.expect(")")
self.skip_spaces_and_comments()
return node
def stmt_atrule(self):
type = self.expect("atrule")
node = Atrule(type)
self.skip_spaces_and_comments()
node.segments = self.selector_parts()
self.skip_spaces_and_comments()
tok = self.peek().type
if tok in ["indent", "{"] or (
tok == "newline" and self.lookahead(2).type == "{"
):
self.state.append("atrule")
node.block = self.block(node)
self.prev_state = self.state[-1]
self.state.pop()
return node
def stmt_scope(self):
self.expect("scope")
parts = []
for literal in self.selector_parts():
parts.append(str(literal))
self.selector_scope = "".join(parts).strip()
return null
def stmt_supports(self):
self.expect("supports")
node = Supports(self.supports_condition())
self.state.append("atrule")
node.block = self.block(node)
self.prev_state = self.state[-1]
self.state.pop()
return node
def supports_condition(self):
node = self.supports_negation()
if node is None:
node = self.supports_op()
if not node:
self.cond = True
node = self.expression()
self.cond = False
return node
def supports_op(self):
feature = self.supports_feature()
if feature:
expr = Expression(lineno=self.lineno, column=self.column)
expr.append(feature)
op = self.accept(["&&", "||"])
while op:
expr.append(
Literal(
"and" if op.value == "&&" else "or",
lineno=self.lineno,
column=self.column,
)
)
expr.append(self.supports_feature())
op = self.accept(["&&", "||"])
return expr
return None
def supports_negation(self):
tok = self.accept("not")
if tok:
node = Expression(lineno=self.lineno, column=self.column)
node.append(Literal("not", lineno=self.lineno, column=self.column))
node.append(self.supports_feature())
return node
return None
def supports_feature(self):
self.skip_spaces_and_comments()
if self.peek().type == "(":
la = self.lookahead(2).type
if la in ["ident", "{"]:
return self.feature()
else:
self.expect("(")
node = Expression(lineno=self.lineno, column=self.column)
node.append(
Literal("(", lineno=self.lineno, column=self.column)
)
node.append(self.supports_condition())
self.expect(")")
node.append(
Literal(")", lineno=self.lineno, column=self.column)
)
self.skip_spaces_and_comments()
return node
return None
def stmt_unless(self):
self.expect("unless")
self.state.append("conditional")
self.cond = True
node = If(
self.expression(), True, lineno=self.lineno, column=self.column
)
self.cond = False
node.block = self.block(node, False)
self.prev_state = self.state[-1]
self.state.pop()
return node
def stmt_for(self):
self.expect("for")
value = self.id().name
key = None
if self.accept(","):
key = self.id().name
self.expect("in")
self.state.append("for")
self.cond = True
each = Each(value, key, self.expression())
self.cond = False
each.block = self.block(each, False)
self.prev_state = self.state[-1]
self.state.pop()
return each
def stmt_if(self):
self.expect("if")
self.state.append("conditional")
self.cond = True
node = If(self.expression(), lineno=self.lineno, column=self.column)
self.cond = False
node.block = self.block(node, False)
self.skip(["newline", "comment"])
while self.accept("else"):
if self.accept("if"):
self.cond = True
cond = self.expression()
self.cond = False
block = self.block(node, False)
node.elses.append(
If(cond, block, lineno=self.lineno, column=self.column)
)
else:
node.elses.append(self.block(node, False))
break
self.skip(["newline", "comment"])
self.prev_state = self.state[-1]
self.state.pop()
return node
def block(self, node, scope=None):
block = self.parent = Block(
self.parent, node, lineno=self.lineno, column=self.column
)
if scope is False:
block.scope = False
self.accept("newline")
# css style
if self.accept("{"):
self.css += 1
delim = "}"
self.skip_whitespace()
else:
delim = "outdent"
self.expect("indent")
while delim != self.peek().type:
# css-style
if self.css > 0:
if self.accept(["newline", "indent"]):
continue
stmt = self.statement()
self.accept(";")
self.skip_whitespace()
else:
if self.accept("newline"):
continue
# skip useless indents and comments
next = self.lookahead(2).type
if "indent" == self.peek().type and next in [
"outdent",
"newline",
"comment",
]:
self.skip(["indent", "outdent"])
continue
if "eos" == self.peek().type:
return block
stmt = self.statement()
self.accept(";")
if not stmt:
self.error("unexpected token {peek} in block")
block.append(stmt)
# css-style
if self.css > 0:
self.skip_whitespace()
self.expect("}")
self.skip_spaces()
self.css -= 1
else:
self.expect("outdent")
self.parent = block.parent
return block
def stmt_selector(self):
scope = self.selector_scope
group = Group(lineno=self.lineno, column=self.column)
is_root = self.current_state() == "root"
while True:
self.accept("newline") # clobber newline after ','
arr = self.selector_parts()
# push the selector
if is_root and scope:
arr.appendleft(
Literal(
f"{scope} ", lineno=self.lineno, column=self.column
)
)
if len(arr) > 0:
selector = Selector(arr)
selector.lineno = arr[0].lineno
selector.column = arr[0].column
group.append(selector)
if self.accept([",", "newline"]) is None:
break
if "selector-parts" == self.current_state():
return group.nodes
self.state.append("selector")
group.block = self.block(group)
self.prev_state = self.state[-1]
self.state.pop()
return group
[docs] def selector_parts(self) -> deque:
"""Selector candidates, stitched together to form a selector."""
arr = deque()
while True:
tok = self.selector_token()
if tok:
if tok.type == "{":
self.skip_spaces()
expr = self.expression()
self.skip_spaces()
self.expect("}")
arr.append(expr)
elif tok.type == self.prefix and ".":
literal = Literal(
tok.value + self.prefix,
lineno=self.lineno,
column=self.column,
)
literal.prefixed = True
arr.append(literal)
elif tok.type == "comment":
# ignore comments
pass
elif tok.type in ["color", "unit"]:
arr.append(
Literal(
tok.value.raw,
lineno=self.lineno,
column=self.column,
)
)
elif tok.type == "space":
arr.append(
Literal(" ", lineno=self.lineno, column=self.column)
)
elif tok.type == "function":
arr.append(
Literal(
f"{tok.value.name}(",
lineno=self.lineno,
column=self.column,
)
)
elif tok.type == "ident":
if hasattr(tok.value, "name") and tok.value.name:
arr.append(
Literal(
f"{tok.value.name}",
lineno=self.lineno,
column=self.column,
)
)
else:
arr.append(
Literal(
f"{tok.value.string}",
lineno=self.lineno,
column=self.column,
)
)
else:
arr.append(
Literal(
tok.value, lineno=self.lineno, column=self.column
)
)
if tok.space:
arr.append(Literal(" "))
else:
break
return arr
def assignment(self):
name = self.id().name
op = self.accept(["=", "?=", "+=", "-=", "*=", "/=", "%="])
if op:
self.state.append("assignment")
expr = self.list()
# @block support
if expr.is_empty():
self.assign_atblock(expr)
node = Ident(name, expr)
self.prev_state = self.state[-1]
self.state.pop()
if op.type == "?=":
defined = BinOp("is defined", node)
lookup = Expression(lineno=self.lineno, column=self.column)
lookup.append(Ident(name))
node = Ternary(defined, lookup, node)
elif op.type in ["+=", "-=", "*=", "/=", "%="]:
node.value = BinOp(op.type[0], Ident(name), expr)
return node
def stmt_ident(self): # noqa: C901
i = 2
la = self.lookahead(i).type
while "space" == la:
i += 1
la = self.lookahead(i).type
go_on = False
if la in ["=", "?=", "-=", "+=", "*=", "/=", "%="]:
# assignment
return self.assignment()
if la == ".":
# member
if "space" == self.lookahead(i - 1).type:
return self.stmt_selector()
if self._ident == self.peek():
return self.id()
i += 1
some_tokens = ["[", ",", "newline", "indent", "eos"]
while (
"=" != self.lookahead(i).type
and self.lookahead(i).type not in some_tokens
):
i += 1
if "=" == self.lookahead(i).type:
self._ident = self.peek()
return self.expression()
elif self.looks_like_selector() and self.state_allows_selector():
return self.stmt_selector()
else:
go_on = True
if la == "[" or go_on:
# assignment []=
if self._ident == self.peek():
return self.id()
while (
"]" != self.lookahead(i).type
and "selector" != self.lookahead(i + 1).type
and "eos" != self.lookahead(i + 1).type
):
i += 1
i += 1
if "=" == self.lookahead(i).type:
self._ident = self.peek()
return self.expression()
elif self.looks_like_selector() and self.state_allows_selector():
return self.stmt_selector()
go_on = True
# operation
if (
la
in [
"-",
"+",
"/",
"*",
"%",
"**",
"&&",
"||",
">",
"<",
">=",
"<=",
"!=",
"==",
"?",
"in",
"is a",
"is defined",
]
or go_on
):
if self._ident == self.peek():
return self.id()
else:
self._ident = self.peek()
if self.current_state() in ["for", "selector"]:
return self.property()
if self.current_state() in ["root", "atblock", "atrule"]:
if la == "[":
return self.subscript()
else:
return self.stmt_selector()
if self.current_state() in ["function", "conditional"]:
if self.looks_like_selector():
return self.stmt_selector()
else:
return self.expression()
if self.operand:
return self.id()
else:
return self.expression()
if self.current_state() == "root":
return self.stmt_selector()
elif self.current_state() in [
"for",
"selector",
"function",
"conditional",
"atblock",
"atrule",
]:
return self.property()
else:
id = self.id()
if "interpolation" == self.previous_state():
id.mixin = True
return id
def property(self):
if self.looks_like_selector(True):
return self.stmt_selector()
# property
ident = self.interpolate()
prop = Property(ident, lineno=self.lineno, column=self.column)
ret = prop
# optional ':'
self.accept("space")
if self.accept(":"):
self.accept("space")
self.state.append("property")
self.in_property = True
prop.expr = self.list()
if len(prop.expr) == 0:
ret = ident[0]
self.in_property = False
self.allow_postfix = True
self.prev_state = self.state[-1]
self.state.pop()
# optional ';'
self.accept(";")
return ret
def interpolate(self):
node = None
segs = []
star = self.accept("*")
if star:
segs.append(Literal("*", lineno=self.lineno, column=self.column))
while True:
if self.accept("{"):
self.state.append("interpolation")
segs.append(self.expression())
self.expect("}")
self.prev_state = self.state[-1]
self.state.pop()
elif self.accept("-"):
segs.append(
Literal("-", lineno=self.lineno, column=self.column)
)
else:
node = self.accept("ident")
if node:
segs.append(node.value)
else:
break
# empty segment list
if len(segs) == 0:
self.expect("ident")
return segs
# fixme!
[docs] def expression(self):
"""negation+"""
expr = Expression(lineno=self.lineno, column=self.column)
self.state.append("expression")
while True:
node = self.negation()
if node is None:
self.error("unexpected token {peek} in expression")
if node.node_name == "nothing":
break
expr.append(node)
self.prev_state = self.state[-1]
self.state.pop()
if len(expr.nodes) > 0:
expr.lineno = expr.nodes[0].lineno
expr.column = expr.nodes[0].column
return expr
def negation(self):
if self.accept("not"):
return UnaryOp("!", self.negation())
return self.ternary()
def ternary(self):
node = self.logical()
if self.accept("?"):
true_expr = self.expression()
self.expect(":")
false_expr = self.expression()
node = Ternary(node, true_expr, false_expr)
return node
[docs] def list(self):
"""expression (',' expression)*"""
node = self.expression()
while self.accept(","):
if node.is_list:
# todo: fixme was list, now node
node.append(self.expression())
else:
list = Expression(true, lineno=self.lineno, column=self.column)
list.append(node)
list.append(self.expression())
node = list
return node
def logical(self):
node = self.typecheck()
while True:
op = self.accept(["&&", "||"])
if op:
node = BinOp(op.type, node, self.typecheck())
else:
break
return node
def typecheck(self):
node = self.equality()
while True:
op = self.accept("is a")
if op:
self.operand = True
if not node:
self.error(
f'illegal unary "{op}", ' f"missing left-hand operand"
)
node = BinOp(op.type, node, self.equality())
self.operand = False
else:
break
return node
def equality(self):
node = self.inn()
while True:
op = self.accept(["==", "!="])
if op:
self.operand = True
if node is None:
self.error(
f'illegal unary "{op}", ' f"missing left-hand operand"
)
node = BinOp(op.type, node, self.inn())
self.operand = False
else:
break
return node
def inn(self):
node = self.relational()
while self.accept("in"):
self.operand = True
if not node:
self.error('illegal unary "in", ' "missing left-hand operand")
node = BinOp("in", node, self.relational())
self.operand = False
return node
def relational(self):
node = self.range()
op = self.accept([">=", "<=", "<", ">"])
if op:
self.operand = True
if not node:
self.error(
f'illegal unary "{op}", ' f"missing left-hand operand"
)
node = BinOp(op.type, node, self.range())
self.operand = False
return node
def stmt_function(self):
parens = 1
i = 2
# Lookahead and determine if we are dealing
# with a function call or definition. Here
# we pair parens to prevent false negatives
# todo: clean this!
try:
tok = self.lookahead(i)
i += 1
while tok:
if tok.type in ["function", "("]:
parens += 1
tok = self.lookahead(i)
i += 1
continue
elif tok.type == ")":
parens -= 1
if parens == 0:
raise TypeError # which is good
tok = self.lookahead(i)
i += 1
continue
elif tok.type == "oes":
self.error('failed to find closing paren ")"')
tok = self.lookahead(i)
i += 1
except TypeError:
pass
if self.current_state() == "expression":
return self.function_call()
else:
if self.looks_like_function_definition(i):
return self.function_definition()
else:
return self.expression()
def range(self):
node = self.additive()
op = self.accept(["...", ".."])
if op:
self.operand = True
if not node:
self.error(f'illegal unary "{op}", missing left-hand operand')
node = BinOp(op.value, node, self.additive())
self.operand = False
return node
def additive(self):
node = self.multiplicative()
op = self.accept(["+", "-"])
while op:
self.operand = True
node = BinOp(op.type, node, self.multiplicative())
self.operand = False
op = self.accept(["+", "-"])
return node
def multiplicative(self):
node = self.defined()
op = self.accept(["**", "*", "/", "%"])
while op:
self.operand = True
if "/" == op.value and self.in_property and self.parens == 0:
self.stash.append(
Token(
"literal",
Literal("/", lineno=self.lineno, column=self.column),
)
)
self.operand = False
return node
else:
if not node:
self.error(
f'illegal unary "{op}", ' f"missing left-hand operand"
)
node = BinOp(op.type, node, self.defined())
self.operand = False
op = self.accept(["**", "*", "/", "%"])
return node
def defined(self):
node = self.unary()
if self.accept("is defined"):
if not node:
self.error(
f'illegal unary "is defined", '
f"missing left-hand operand"
)
node = BinOp("is defined", node)
return node
def unary(self):
op = self.accept(["!", "~", "+", "-"])
if op:
self.operand = True
node = self.unary()
if node is None:
self.error(f'illegal unary "{op}"')
node = UnaryOp(op.type, node)
self.operand = False
return node
return self.subscript()
def subscript(self):
node = self.member()
while self.accept("["):
node = BinOp("[]", node, self.expression())
self.expect("]")
if self.accept("="):
node.op += "="
node.value = self.list()
# @block suppprt
if node.value.is_empty():
self.assign_atblock(node.value)
return node
def member(self):
node = self.primary()
if node and node.node_name != "nothing":
while self.accept("."):
id = Ident(self.expect("ident").value.string)
node = Member(node, id)
self.skip_spaces()
if self.accept("="):
node.value = self.list()
# @block support
if node.value.is_empty():
self.assign_atblock(node.value)
return node
def primary(self):
self.skip_spaces()
# parenthesis
if self.accept("("):
self.parens += 1
expr = self.expression()
paren = self.expect(")")
self.parens -= 1
if self.accept("%"):
expr.append(Ident("%"))
tok = self.peek()
# (1 + 2)px, (1 + 2)em, etc.
if (
not paren.space
and "ident" == tok.type
and tok.value.string in units
):
expr.append(Ident(tok.value.string))
self.next()
return expr
tok = self.peek()
# primitive
if tok.type in [
"null",
"unit",
"color",
"string",
"literal",
"boolean",
"comment",
]:
return self.next().value
elif not self.cond and tok.type == "{":
return self.object()
elif tok.type == "atblock":
return self.atblock()
# property lookup
elif tok.type == "atrule":
id = Ident(self.next().value)
id.property = True
return id
elif tok.type == "ident":
return self.stmt_ident()
elif tok.type == "function":
if tok.anonymous:
return self.function_definition()
else:
return self.function_call()
return Nothing()
def function_definition(self):
name = self.expect("function").value.name
# params
self.state.append("function params")
self.skip_whitespace()
params = self.params()
self.skip_whitespace()
self.expect(")")
self.prev_state = self.state[-1]
self.state.pop()
# body
self.state.append("function")
fn = Function(
name, params, builtin=False, lineno=self.lineno, column=self.column
)
fn.block = self.block(fn)
self.prev_state = self.state[-1]
self.state.pop()
return Ident(name, fn)
def id(self):
tok = self.expect("ident")
self.accept("space")
return tok.value
def atblock(self, node=None):
if node is None:
self.expect("atblock")
n = Atblock()
self.state.append("atblock")
n.block = self.block(n, scope=False)
self.prev_state = self.state[-1]
self.state.pop()
return n
def return_expression(self):
self.expect("return")
expr = self.expression()
if expr.is_empty():
return ReturnNode()
else:
return ReturnNode(expr)
pass
def keyframes(self):
tok = self.expect("keyframes")
self.skip_spaces_and_comments()
keyframes = Keyframes(self.selector_parts(), tok.value)
self.skip_spaces_and_comments()
# block
self.state.append("atrule")
keyframes.block = self.block(keyframes)
self.prev_state = self.state[-1]
self.state.pop()
return keyframes
def args(self):
args = Arguments()
while True:
if self.peek().type == "ident" and self.lookahead(2).type == ":":
keyword = self.next().value.string
self.expect(":")
args.map[keyword] = self.expression()
else:
args.append(self.expression())
if self.accept(",") is None:
break
return args
def url(self):
self.expect("function")
self.state.append("function")
args = self.args()
self.expect(")")
self.prev_state = self.state[-1]
self.state.pop()
return Call("url", args, lineno=self.lineno, column=self.column)
def mozdocument(self):
self.expect("-moz-document")
mozdocument = Atrule("-moz-document")
calls = []
while True:
self.skip_spaces_and_comments()
calls.append(self.function_call())
self.skip_spaces_and_comments()
if self.accept(",") is None:
break
joined_calls = [str(call) for call in calls]
mozdocument.segments = [
Literal(
", ".join(joined_calls), lineno=self.lineno, column=self.column
)
]
self.state.append("atrule")
mozdocument.block = self.block(mozdocument, False)
self.prev_state = self.state[-1]
self.state.pop()
return mozdocument
def function_call(self):
with_block = self.accept("+")
if self.peek().value.name == "url":
return self.url()
name = self.expect("function").value.name
self.state.append("function arguments")
self.parens += 1
args = self.args()
self.expect(")")
self.parens -= 1
self.prev_state = self.state[-1]
self.state.pop()
call = Call(name, args, lineno=self.lineno, column=self.column)
if with_block:
self.state.append("function")
call.block = self.block(call)
self.prev_state = self.state[-1]
self.state.pop()
return call
def params(self):
params = Params()
tok = self.accept("ident")
while tok:
self.accept("space")
node = tok.value
params.append(node)
if self.accept("..."):
node.rest = True
elif self.accept("="):
node.value = self.expression()
self.skip_whitespace()
self.accept(",")
self.skip_whitespace()
tok = self.accept("ident")
params.lineno = self.lineno
params.column = self.column
return params
def object(self):
comma = None
obj = ObjectNode({})
self.expect("{")
self.skip_whitespace()
while True:
token = self.next()
if token and token.type == "}":
break
if token.type in ["comment", "newline"]:
continue
if not comma and token.type == ",":
continue
if token.type in ["ident", "string"]:
id = token
if id is None:
self.error('expected "ident" or "string", got {peek}')
id = id.value.hash()
self.skip_spaces_and_comments()
self.expect(":")
value = self.expression()
obj.set(id, value)
comma = self.accept(",")
self.skip_whitespace()
return obj
def _operation(self, la):
# operation
# prevent cyclic .ident, return literal
if self._ident == self.peek():
return self.id()
else:
self._ident = self.peek()
if self.current_state() in ["for", "selector"]:
return self.property()
elif self.current_state() in ["root", "atblock", "atrule"]:
if "[" == la:
return self.subscript()
else:
return self.stmt_selector()
elif self.current_state() in ["function", "conditional"]:
if self.looks_like_selector():
return self.stmt_selector()
else:
return self.expression()
else:
# do not disrupt the ident when an operand
if self.operand:
return self.id()
else:
return self.expression()