def UnaryExpression(tokenizer, staticContext): tokenType = tokenizer.get(True) if tokenType == "not": node = Node.Node(tokenizer, tokenType) child = UnaryExpression(tokenizer, staticContext) # Support for prefixes with "not" e.g. "!important" if child.type == "identifier": child.value = "!%s" % child.value return child else: node.append(child) elif tokenType in ("plus", "minus"): if tokenType == "plus": tokenType = "unary_plus" elif tokenType == "minus": tokenType = "unary_minus" node = Node.Node(tokenizer, tokenType) node.append(UnaryExpression(tokenizer, staticContext)) else: tokenizer.unget() node = MemberExpression(tokenizer, staticContext) return node
def __extendContent(node, call, targetBlock, stopCombineAt): """ Builds up a list of selector/@media/@support to insert after the extend to produce the @content sections on the intended selectors. """ for child in reversed(list(node)): if child: __extendContent(child, call, targetBlock, stopCombineAt) if node.type == "content" and hasattr(call, "rules"): # Extends support @content as well. In this case we produce a new selector # which matches the position of the content section and append it after # the original extended mixin on return Console.debug("Inserting content section into new virtual selector") selector, media, supports = Util.combineSelector(node, stop=stopCombineAt) selectorNode = Node.Node(type="selector") selectorNode.name = selector selectorNode.append(copy.deepcopy(call.rules), "rules") # Support @supports if supports: supportsNode = Node.Node(type="supports") supportsNode.name = supports supportsBlock = Node.Node(type="block") supportsBlock.append(selectorNode) supportsNode.append(supportsBlock, "rules") # Update reference selectorNode = supportsNode # Support @media if media: mediaNode = Node.Node(type="media") mediaNode.name = media mediaBlock = Node.Node(type="block") mediaBlock.append(selectorNode) mediaNode.append(mediaBlock, "rules") # Update reference selectorNode = mediaNode # Insert selectorNode (or media node or supports node when updated) # If all kinds are used we should have the following structure: # @media->@supports->selector targetBlock.append(selectorNode)
def KeyFrames(tokenizer, staticContext): """ Supports e.g.: @keyframes fade{ from, 10%{ background-color: #000000; } 100%{ background-color: #FFFFFF; } } """ node = Node.Node(tokenizer, "keyframes") node.vendor = Util.extractVendor(tokenizer.token.value) # Use param as name on keyframes tokenizer.get() node.name = tokenizer.token.value tokenizer.mustMatch("left_curly") while tokenizer.get() != "right_curly": # Parse frame as block frameNode = Node.Node(tokenizer, "frame") token = tokenizer.token frameNode.value = "%s%s" % (token.value, getattr(token, "unit", "")) node.append(frameNode) # Process comma separated values for while True: if tokenizer.peek() != "comma": break else: tokenizer.mustMatch("comma") # Next one is our next value tokenizer.get() token = tokenizer.token frameNode.value += ",%s%s" % (token.value, getattr(token, "unit", "")) # Next process content of selector blockNode = Block(tokenizer, staticContext) frameNode.append(blockNode, "rules") return node
def AddExpression(tokenizer, staticContext): node = MultiplyExpression(tokenizer, staticContext) if node.type == "identifier": return node while tokenizer.match("plus") or tokenizer.match("minus"): # Whether there was skipped spaces before skippedA = tokenizer.skippedSpaces or tokenizer.skippedLineBreaks # ...but not after the plus/minus token peek = tokenizer.peek() skippedB = tokenizer.skippedSpaces or tokenizer.skippedLineBreaks # ... then do not interpret as plus/minus expression but as unary prefix if skippedA and not skippedB: tokenizer.unget() return node # Build real plus/minus node childNode = Node.Node(tokenizer) childNode.append(node) express = MultiplyExpression(tokenizer, staticContext) if express.type == "identifier": raise ParseError("Invalid expression", tokenizer) childNode.append(express) node = childNode return node
def MemberExpression(tokenizer, staticContext): node = PrimaryExpression(tokenizer, staticContext) while True: tokenType = tokenizer.get() if tokenType == "end": break # system calls elif tokenType == "left_paren": if node.type == "identifier": childNode = Node.Node(tokenizer, "function") childNode.name = node.value # Special processing of URL commands if node.value == "url": childNode.append(UrlArgumentList(tokenizer, staticContext), "params") else: childNode.append(CssArgumentList(tokenizer, staticContext), "params") elif node.type == "command": if node.name == "raw": childNode = RawArgument(tokenizer, staticContext) elif node.name == "expr": childNode = ExpressionArgument(tokenizer, staticContext) else: childNode = Node.Node(tokenizer, "command") childNode.name = node.name childNode.append(ArgumentList(tokenizer, staticContext), "params") else: raise ParseError( "Unsupported mixin include in expression statement", tokenizer) else: tokenizer.unget() return node node = childNode return node
def FontFace(tokenizer, staticContext): # Like a selector but store as a different type node = node = Node.Node(tokenizer, "fontface") childNode = Block(tokenizer, staticContext) node.append(childNode, "rules") return node
def castNativeToNode(value): if value is True: node = Node.Node(type="true") elif value is False: node = Node.Node(type="false") elif isinstance(value, str): node = Node.Node(type="string") node.value = value elif isinstance(value, (float, int)): node = Node.Node(type="number") node.value = value elif value is None: node = Node.Node(type="null") else: raise ResolverError("Could not transform field %s=%s to style value" % (name, value)) return node
def UrlArgumentList(tokenizer, staticContext): node = Node.Node(tokenizer, "list") if tokenizer.match("right_paren", True): return node url = "" while True: tokenType = tokenizer.get() if tokenType == "right_paren": break elif tokenType == "string": token = tokenizer.token url += token.value elif tokenType == "number": token = tokenizer.token url += "%s%s" % (token.value, getattr(token, "unit", "")) elif tokenType == "div": url += "/" elif tokenType == "dot": url += "." elif tokenType == "identifier": token = tokenizer.token url += token.value elif tokenType == "variable": # Fast path when variable present token = tokenizer.token var = Node.Node(tokenizer) var.name = token.value node.append(var) tokenizer.mustMatch("right_paren") return node else: token = tokenizer.token raise ParseError( "Invalid token in URL parameter: Type = %s ; Value = %s" % (token.type, getattr(token, "value", None)), tokenizer) urlParam = Node.Node(tokenizer, "identifier") urlParam.value = url node.append(urlParam) return node
def OrExpression(tokenizer, staticContext): node = AndExpression(tokenizer, staticContext) while tokenizer.match("or"): childNode = Node.Node(tokenizer, "or") childNode.append(node) childNode.append(AndExpression(tokenizer, staticContext)) node = childNode return node
def ExpressionArgument(tokenizer, staticContext): if tokenizer.match("right_paren", True): raise ParseError("Expected expression", tokenizer) node = Node.Node(tokenizer, "expr") node.append(AddExpression(tokenizer, staticContext)) tokenizer.mustMatch("right_paren") return node
def AndExpression(tokenizer, staticContext): node = EqualityExpression(tokenizer, staticContext) while tokenizer.match("and"): childNode = Node.Node(tokenizer, "and") childNode.append(node) childNode.append(EqualityExpression(tokenizer, staticContext)) node = childNode return node
def executeCommand(node, profile): command = node.name params = [] for param in node.params: # Variable not yet processed (possible e.g. during permutation apply) if param.type == "variable": return node elif param.type == "unary_minus": value = -param[0].value elif hasattr(param, "value"): value = param.value else: raise Exception( "Invalid value for command execution: Type is %s in %s!" % (param.type, param.line)) params.append(value) # Catch simple casting requests if command in ("identifier", "string", "number"): if len(params) != 1: raise Exception("Invalid number of arguments for type casting!") repl = Node.Node(type=command) repl.value = params[0] return repl # print("Looking for command: %s(%s)" % (command, ", ".join([str(param) for param in params]))) result, restype = profile.executeCommand(command, params) if restype == "px": repl = Node.Node(type="number") repl.value = result repl.unit = restype elif restype == "url": repl = Node.Node(type="function") repl.name = "url" listChild = Node.Node(type="list") repl.append(listChild, "params") valueChild = Node.Node(type="identifier") valueChild.value = result listChild.append(valueChild) elif restype == "number": repl = Node.Node(type="number") repl.value = result else: repl = Node.Node(type="identifier") repl.value = result return repl
def PrimaryExpression(tokenizer, staticContext): tokenType = tokenizer.get(True) if tokenType == "function": node = FunctionDefinition(tokenizer, staticContext, False, "expressed_form") elif tokenType == "left_paren": # ParenExpression does its own matching on parentheses, so we need to unget. tokenizer.unget() node = ParenExpression(tokenizer, staticContext) node.parenthesized = True elif tokenType == "variable": node = Node.Node(tokenizer, tokenType) node.name = tokenizer.token.value elif tokenType == "command": node = Node.Node(tokenizer, tokenType) node.name = tokenizer.token.value elif tokenType in [ "null", "true", "false", "identifier", "number", "string", "div" ]: node = Node.Node(tokenizer, tokenType) if tokenType in ("identifier", "string", "number"): node.value = tokenizer.token.value if tokenType == "number" and hasattr(tokenizer.token, "unit"): node.unit = tokenizer.token.unit if tokenType == "string": node.quote = tokenizer.token.quote if tokenType == "div": node.type = "slash" else: raise ParseError("Missing operand. Found type: %s" % tokenType, tokenizer) return node
def Supports(tokenizer, staticContext): node = Node.Node(tokenizer, "supports") tokenType = tokenizer.get() test = "" requiresSpace = False while tokenType != "left_curly": token = tokenizer.token if tokenType == "identifier": if requiresSpace: test += " " test += token.value requiresSpace = True elif tokenType == "colon": test += ":" requiresSpace = False elif tokenType == "left_paren": if requiresSpace: test += " " test += "(" requiresSpace = False elif tokenType == "right_paren": test += ")" requiresSpace = True elif tokenType == "string": if requiresSpace: test += " " test += ascii_encoder.encode(token.value) requiresSpace = True elif tokenType == "number": if requiresSpace: test += " " test += "%s%s" % (token.value, getattr(token, "unit", "")) requiresSpace = True else: raise ParseError("Unsupported supports token %s" % tokenType, tokenizer) tokenType = tokenizer.get() # Split at commas, but ignore any white spaces (trim single selectors) node.name = test # Next process content of selector tokenizer.unget() childNode = Block(tokenizer, staticContext) node.append(childNode, "rules") return node
def EqualityExpression(tokenizer, staticContext): node = RelationalExpression(tokenizer, staticContext) while tokenizer.match("eq") or tokenizer.match("ne"): childNode = Node.Node(tokenizer) childNode.append(node) express = RelationalExpression(tokenizer, staticContext) if express.type == "identifier": raise ParseError("Invalid expression", tokenizer) childNode.append(express) node = childNode return node
def CssArgumentList(tokenizer, staticContext): node = Node.Node(tokenizer, "list") if tokenizer.match("right_paren", True): return node while True: collection = Node.Node(tokenizer, "list") while True: childNode = AssignExpression(tokenizer, staticContext) collection.append(childNode) # Comma ends the collection + Right paren ends the list if tokenizer.peek() in ("comma", "right_paren"): break node.append(collection) if not tokenizer.match("comma"): break tokenizer.mustMatch("right_paren") return node
def ArgumentList(tokenizer, staticContext): node = Node.Node(tokenizer, "list") if tokenizer.match("right_paren", True): return node while True: childNode = AssignExpression(tokenizer, staticContext) node.append(childNode) if not tokenizer.match("comma"): break tokenizer.mustMatch("right_paren") return node
def Page(tokenizer, staticContext): """ Supports e.g.: @page{ margin: 1cm; } @page :first{ margin: 5cm 2cm; } """ node = Node.Node(tokenizer, "page") tokenType = tokenizer.get() selector = "" requiresSpace = False while tokenType != "left_curly": token = tokenizer.token if tokenType == "identifier": if requiresSpace: selector += " " selector += token.value requiresSpace = True elif tokenType == "colon": selector += ":" requiresSpace = False else: raise ParseError("Unsupported page selector token %s" % tokenType, tokenizer) tokenType = tokenizer.get() # Set page selector as name node.name = selector # Next process content of selector tokenizer.unget() childNode = Block(tokenizer, staticContext) node.append(childNode, "rules") return node
def MultiplyExpression(tokenizer, staticContext): node = UnaryExpression(tokenizer, staticContext) if node.type == "identifier": return node while tokenizer.match("mul") or tokenizer.match("div") or tokenizer.match( "mod"): childNode = Node.Node(tokenizer) childNode.append(node) express = UnaryExpression(tokenizer, staticContext) if express.type == "identifier": raise ParseError("Invalid expression", tokenizer) childNode.append(express) node = childNode return node
def AssignExpression(tokenizer, staticContext): comments = tokenizer.getComments() node = Node.Node(tokenizer, "assign") lhs = OrExpression(tokenizer, staticContext) addComments(lhs, None, comments) if not tokenizer.match("assign"): return lhs if lhs.type == "variable": pass else: raise ParseError("Bad left-hand side of assignment", tokenizer) node.assignOp = tokenizer.token.assignOp node.append(lhs) node.append(AssignExpression(tokenizer, staticContext)) return node
def Charset(tokenizer, staticContext): tokenType = tokenizer.get() Console.warn("CSS @charset %s ", tokenType) if tokenType != "string": raise ParseError( "Invalid @charset declaration. Requires the encoding being a string!", tokenizer) encoding = tokenizer.token.value if encoding.lower() != "utf-8": raise ParseError("Jasy is not able to process non UTF-8 stylesheets!", tokenizer) Console.warn("Found unnecessary @charset definition for encoding %s", encoding) return Node.Node(tokenizer, "block")
def RelationalExpression(tokenizer, staticContext): # Uses of the in operator in shiftExprs are always unambiguous, # so unset the flag that prohibits recognizing it. node = AddExpression(tokenizer, staticContext) if node.type == "identifier": return node while tokenizer.match("lt") or tokenizer.match("le") or tokenizer.match( "ge") or tokenizer.match("gt"): childNode = Node.Node(tokenizer) childNode.append(node) express = AddExpression(tokenizer, staticContext) if express.type == "identifier": raise ParseError("Invalid expression", tokenizer) childNode.append(express) node = childNode return node
def Expression(tokenizer, staticContext): """ Top-down expression parser for stylestyles. """ node = AssignExpression(tokenizer, staticContext) if tokenizer.match("comma"): childNode = Node.Node(tokenizer, "comma") childNode.append(node) node = childNode while True: childNode = node[len(node) - 1] node.append(AssignExpression(tokenizer, staticContext)) if not tokenizer.match("comma"): break return node
def Statements(tokenizer, staticContext): """Parses a list of Statements.""" node = Node.Node(tokenizer, "block") staticContext.blockId += 1 staticContext.statementStack.append(node) prevNode = None while not tokenizer.done() and tokenizer.peek(True) != "right_curly": comments = tokenizer.getComments() childNode = Statement(tokenizer, staticContext) # Ignore semicolons in AST if childNode.type != "semicolon": node.append(childNode) prevNode = childNode staticContext.statementStack.pop() return node
def __resolveMixin(mixin, params): """Returns a clone of the given mixin and applies optional parameters to it.""" # Generate random prefix for variables and parameters chars = string.ascii_letters + string.digits prefix = ''.join(random.sample(chars * 6, 6)) # Data base of all local variable and parameter name mappings variables = {} # Generate full recursive clone of mixin rules clone = copy.deepcopy(mixin.rules) if hasattr(mixin, "params"): for pos, param in enumerate(mixin.params): # We have to copy over the parameter value as a local variable declaration paramAsDeclaration = Node.Node(type="declaration") if param.type == "variable": paramAsDeclaration.name = param.name elif param.type == "assign" and param[0].type == "variable": paramAsDeclaration.name = param[0].name else: raise Exception( "Unsupported param structure for mixin resolver at line %s! Expected type variable or assignment and got: %s!" % (mixin.line, param.type)) # Copy over actual param value if len(params) > pos: paramAsDeclaration.append(copy.deepcopy(params[pos]), "initializer") elif param.type == "assign" and param[0].type == "variable": paramAsDeclaration.append(copy.deepcopy(param[1]), "initializer") clone.insert(0, paramAsDeclaration) __renameRecurser(clone, variables, prefix) return clone
def Property(tokenizer, staticContext): """ Parses all CSS properties e.g. - background: red - font: 12px bold Arial; """ node = Node.Node(tokenizer, "property") node.name = "" # Start from the beginning to support mixed identifiers/variables easily tokenizer.unget() while tokenizer.match("variable") or tokenizer.match("identifier"): token = tokenizer.token if token.type == "variable": node.name += "${%s}" % token.value if hasattr(node, "dynamic"): node.dynamic.add(token.value) else: node.dynamic = set([token.value]) else: node.name += token.value if not tokenizer.mustMatch("colon"): raise ParseError("Invalid property definition", tokenizer) # Add all values until we find a semicolon or right curly while tokenizer.peek() not in ("semicolon", "right_curly"): childNode = ValueExpression(tokenizer, staticContext) node.append(childNode) return node
def Root(tokenizer, staticContext): """ Supports e.g.: h1{ font-size: 20px; @root html.desktop &{ font-size: 30px; } } .date{ color: black; background: white; @root{ &__dialog{ position: absolute; } } } """ node = Node.Node(tokenizer, "root") tokenType = tokenizer.get() if tokenType != "left_curly": node.append(Selector(tokenizer, staticContext)) else: tokenizer.unget() childNode = Block(tokenizer, staticContext) node.append(childNode, "rules") return node
def __flatter(node, dest): """Moves all selectors to the top tree node while keeping media queries intact and/or making them CSS2 compatible (regarding structure)""" process = node.type in ("selector", "mixin", "media", "supports") # Insert all children of top-level nodes into a helper element first # This is required to place mixins first, before the current node and append # all remaining nodes afterwards if process: chdest = Node.Node(None, "helper") else: chdest = dest # Process children first if len(node) > 0: for child in list(node): if child is not None: __flatter(child, chdest) # Filter out empty nodes from processing if process and hasattr(node, "rules") and len(node.rules) > 0: # Combine selector and/or media query combinedSelector, combinedMedia, combinedSupports = Util.combineSelector( node) if node.type == "selector": node.name = combinedSelector elif node.type == "mixin": node.selector = combinedSelector elif node.type == "media": pass elif node.type == "supports": pass if (combinedMedia or combinedSupports) and node.type in ("selector", "mixin"): if combinedSupports: # Dynamically create matching media query supportsNode = Node.Node(None, "supports") supportsNode.name = combinedSupports supportsBlock = Node.Node(None, "block") supportsNode.append(supportsBlock, "rules") supportsBlock.append(node) node = supportsNode if combinedMedia: # Dynamically create matching media query mediaNode = Node.Node(None, "media") mediaNode.name = combinedMedia mediaBlock = Node.Node(None, "block") mediaNode.append(mediaBlock, "rules") mediaBlock.append(node) node = mediaNode elif node.type == "media" or node.type == "supports": # Insert direct properties into new selector block # Goal is to place in this structure: @media->@supports->selector # Update media query of found media queries as it might # contain more than the local one (e.g. queries in parent nodes) if node.type == "media": node.name = combinedMedia # Update support query of found supports query as it might # contain more than the local one (e.g. queries in parent nodes) elif node.type == "supports": node.name = combinedSupports # Create new selector node where we move all rules into selectorNode = Node.Node(None, "selector") selectorNode.name = combinedSelector selectorBlock = Node.Node(None, "block") selectorNode.append(selectorBlock, "rules") # Move all rules from local media/supports block into new selector block for nonSelectorChild in list(node.rules): if nonSelectorChild: selectorBlock.append(nonSelectorChild) if node.type == "supports" and combinedMedia: # Dynamically create matching mediaquery node mediaNode = Node.Node(None, "media") mediaNode.name = combinedMedia mediaBlock = Node.Node(None, "block") mediaNode.append(mediaBlock, "rules") # Replace current node with media node node.parent.replace(node, mediaNode) # Then append this node to the media node mediaBlock.append(node) # Selector should be placed inside this node node.rules.append(selectorNode) # Update node reference to new outer node for further processing node = mediaNode elif node.type == "media" and combinedSupports: # Dynamically create matching supports node supportsNode = Node.Node(None, "supports") supportsNode.name = combinedSupports supportsBlock = Node.Node(None, "block") supportsNode.append(supportsBlock, "rules") # Move supports node into this node node.rules.append(supportsNode) # The supports block is the parent of the selector supportsBlock.append(selectorNode) else: node.rules.append(selectorNode) if process: # Place any mixins before the current node for child in list(chdest): if child.type == "mixin": dest.append(child) # The append self dest.append(node) # Afterwards append any children for child in list(chdest): dest.append(child)
def process(tree): """Flattens selectors to that `h1{ span{ ...` is merged into `h1 span{ ...`""" Console.info("Flattening selectors...") Console.indent() def __flatter(node, dest): """Moves all selectors to the top tree node while keeping media queries intact and/or making them CSS2 compatible (regarding structure)""" process = node.type in ("selector", "mixin", "media", "supports") # Insert all children of top-level nodes into a helper element first # This is required to place mixins first, before the current node and append # all remaining nodes afterwards if process: chdest = Node.Node(None, "helper") else: chdest = dest # Process children first if len(node) > 0: for child in list(node): if child is not None: __flatter(child, chdest) # Filter out empty nodes from processing if process and hasattr(node, "rules") and len(node.rules) > 0: # Combine selector and/or media query combinedSelector, combinedMedia, combinedSupports = Util.combineSelector( node) if node.type == "selector": node.name = combinedSelector elif node.type == "mixin": node.selector = combinedSelector elif node.type == "media": pass elif node.type == "supports": pass if (combinedMedia or combinedSupports) and node.type in ("selector", "mixin"): if combinedSupports: # Dynamically create matching media query supportsNode = Node.Node(None, "supports") supportsNode.name = combinedSupports supportsBlock = Node.Node(None, "block") supportsNode.append(supportsBlock, "rules") supportsBlock.append(node) node = supportsNode if combinedMedia: # Dynamically create matching media query mediaNode = Node.Node(None, "media") mediaNode.name = combinedMedia mediaBlock = Node.Node(None, "block") mediaNode.append(mediaBlock, "rules") mediaBlock.append(node) node = mediaNode elif node.type == "media" or node.type == "supports": # Insert direct properties into new selector block # Goal is to place in this structure: @media->@supports->selector # Update media query of found media queries as it might # contain more than the local one (e.g. queries in parent nodes) if node.type == "media": node.name = combinedMedia # Update support query of found supports query as it might # contain more than the local one (e.g. queries in parent nodes) elif node.type == "supports": node.name = combinedSupports # Create new selector node where we move all rules into selectorNode = Node.Node(None, "selector") selectorNode.name = combinedSelector selectorBlock = Node.Node(None, "block") selectorNode.append(selectorBlock, "rules") # Move all rules from local media/supports block into new selector block for nonSelectorChild in list(node.rules): if nonSelectorChild: selectorBlock.append(nonSelectorChild) if node.type == "supports" and combinedMedia: # Dynamically create matching mediaquery node mediaNode = Node.Node(None, "media") mediaNode.name = combinedMedia mediaBlock = Node.Node(None, "block") mediaNode.append(mediaBlock, "rules") # Replace current node with media node node.parent.replace(node, mediaNode) # Then append this node to the media node mediaBlock.append(node) # Selector should be placed inside this node node.rules.append(selectorNode) # Update node reference to new outer node for further processing node = mediaNode elif node.type == "media" and combinedSupports: # Dynamically create matching supports node supportsNode = Node.Node(None, "supports") supportsNode.name = combinedSupports supportsBlock = Node.Node(None, "block") supportsNode.append(supportsBlock, "rules") # Move supports node into this node node.rules.append(supportsNode) # The supports block is the parent of the selector supportsBlock.append(selectorNode) else: node.rules.append(selectorNode) if process: # Place any mixins before the current node for child in list(chdest): if child.type == "mixin": dest.append(child) # The append self dest.append(node) # Afterwards append any children for child in list(chdest): dest.append(child) def __clean(node): """ Removes all empty rules. Starting from inside out for a deep cleanup. This is a required step for the next one where we combine media queries and selectors and need to have an easy reference point to the previous node. """ # Process children first for child in reversed(node): if child is not None: __clean(child) if hasattr(node, "rules") and len(node.rules) == 0: Console.debug( "Cleaning up empty selector/mixin/@media/@supports at line %s" % node.line) node.parent.remove(node) elif node.type == "content": Console.debug("Cleaning up left over @content at line %s" % node.line) node.parent.remove(node) elif node.type == "meta": Console.debug("Cleaning up left over @meta at line %s" % node.line) node.parent.remove(node) elif node.type == "block" and node.parent.type in ("sheet", "block"): Console.debug( "Inlining content of unnecessary block node at line %s" % node.line) node.parent.insertAllReplace(node, node) elif node.type == "root" and len(node) == 0: Console.debug("Cleaning up left over @root at line %s" % node.line) node.parent.remove(node) def __combine(tree, top=True): """Combines follow up selector/media/supports nodes with the same name.""" previousSelector = None previousMedia = None previousSupports = None # Work on a copy to be safe for remove situations during merges previousChild = None for child in list(tree): if not child: continue if child.type == "selector" or child.type == "mixin": if child.type == "selector": thisSelector = child.name elif child.type == "mixin": thisSelector = child.selector if thisSelector == previousSelector: previousChild.rules.insertAll(None, child.rules) tree.remove(child) Console.debug("Combined selector of line %s into %s" % (child.line, previousChild.line)) else: previousChild = child previousSelector = thisSelector previousMedia = None previousSupports = None elif child.type == "media": if child.name == previousMedia: previousChild.rules.insertAll(None, child.rules) tree.remove(child) Console.debug("Combined @media of line %s into %s" % (child.line, previousChild.line)) else: previousChild = child previousMedia = child.name previousSelector = None previousSupports = None elif child.type == "supports": if child.name == previousSupports: previousChild.rules.insertAll(None, child.rules) tree.remove(child) Console.debug("Combined @supports of line %s into %s" % (child.line, previousChild.line)) else: previousChild = child previousSupports = child.name previousSelector = None previousMedia = None else: previousChild = None previousSelector = None previousSupports = None previousMedia = None # Re-run combiner inside all media queries. # Selectors in there are allowed and could be combined, too if top: for child in tree: if child and (child.type == "media" or child.type == "supports"): __combine(child.rules, False) # Execute the different features in order dest = Node.Node(None, "sheet") __flatter(tree, dest) tree.insertAll(0, dest) __clean(tree) __combine(tree) Console.outdent() return
def compute(node, first=None, second=None, operator=None, session=None): """ Recursively processes given operation node. Consumes optional hints for the first/second child of an operation as well as the operator itself (in cases where it could not be figured out automatically). The session is useful for supporting commands inside of operations. """ # Fill gaps in empty arguments if operator is None: operator = node.type # Fill missing first/second param if first is None and len(node) >= 1: first = node[0] if second is None and len(node) >= 2: second = node[1] # Error handling if node is None or operator is None: raise OperationError("Missing arguments for operation compute()", node) # Solve inner operations first if first is not None: if first.type in Util.ALL_OPERATORS: first = compute(first, session=session) elif first.type == "command": first = Util.executeCommand(first, session) if second is not None: if second.type in Util.ALL_OPERATORS: second = compute(second, session=session) elif second.type == "command": second = Util.executeCommand(second, session) # Support for not-/and-/or-operator if operator == "not": return Util.castNativeToNode(not castToBool(first)) elif operator == "and": return Util.castNativeToNode(castToBool(first) and castToBool(second)) elif operator == "or": return Util.castNativeToNode(castToBool(first) or castToBool(second)) # Support for default set operator "?=" when variable was not defined before elif operator == "questionmark" and first is None: return second # Ignore when not yet processed if first.type in ("command", "variable") or second.type in ("command", "variable"): return # Compare operation types elif first.type == second.type: if first.type in ("true", "false", "null"): if operator in ("eq", "ge", "le"): return Util.castNativeToNode(True) else: return Util.castNativeToNode(False) elif first.type == "number": firstUnit = getattr(first, "unit", None) secondUnit = getattr(second, "unit", None) if operator in Util.COMPARE_OPERATORS: if firstUnit == secondUnit or firstUnit is None or secondUnit is None: if operator == "eq": return Util.castNativeToNode(first.value == second.value) elif operator == "ne": return Util.castNativeToNode(first.value != second.value) elif operator == "gt": return Util.castNativeToNode(first.value > second.value) elif operator == "lt": return Util.castNativeToNode(first.value < second.value) elif operator == "ge": return Util.castNativeToNode(first.value >= second.value) elif operator == "le": return Util.castNativeToNode(first.value <= second.value) else: raise OperationError("Unsupported unit combination for number comparison", node) elif firstUnit == secondUnit or firstUnit is None or secondUnit is None: if operator in Util.MATH_OPERATORS: repl = Node.Node(type="number") if firstUnit is not None: repl.unit = firstUnit elif secondUnit is not None: repl.unit = secondUnit if operator == "plus": repl.value = first.value + second.value elif operator == "minus": repl.value = first.value - second.value elif operator == "mul": repl.value = first.value * second.value elif operator == "div": repl.value = first.value / second.value elif operator == "mod": repl.value = first.value % second.value return repl elif operator == "questionmark": return first else: raise OperationError("Unsupported number operation", node) elif firstUnit == "%" or secondUnit == "%": if operator in ("mul", "div"): repl = Node.Node(type="number") if operator == "mul": repl.value = first.value * second.value / 100 elif operator == "mul": repl.value = first.value / second.value / 100 if firstUnit == "%": repl.unit = secondUnit else: repl.unit = firstUnit return repl else: raise OperationError("Could not compute mixed percent operations for operators other than \"*\" and \"/\"", node) else: raise OperationError("Could not compute result from numbers of different units: %s vs %s" % (first.unit, second.unit), node) elif first.type == "string": if operator == "plus": repl = Node.Node(type="string") repl.value = first.value + second.value return repl elif operator == "eq": return Util.castNativeToNode(first.value == second.value) elif operator == "ne": return Util.castNativeToNode(first.value != second.value) else: raise OperationError("Unsupported string operation", node) elif first.type == "list": if len(first) == len(second): repl = Node.Node(type="list") for pos, child in enumerate(first): childRepl = compute(node, first=child, second=second[pos], session=session) if childRepl is not None: repl.append(childRepl) return repl else: raise OperationError("For list operations both lists have to have the same length!", node) else: raise OperationError("Unsupported operation on %s" % first.type, node) elif first.type == "true" and second.type == "false": return Util.castNativeToNode(False) elif first.type == "false" and second.type == "true": return Util.castNativeToNode(False) elif first.type == "list" and second.type != "list": repl = Node.Node(type="list") for child in first: childRepl = compute(node, first=child, second=second, session=session) if childRepl is not None: repl.append(childRepl) return repl elif first.type != "list" and second.type == "list": repl = Node.Node(type="list") for child in second: childRepl = compute(node, first=first, second=child, session=session) if childRepl is not None: repl.append(childRepl) return repl elif first.type == "string" or second.type == "string": repl = Node.Node(type="string") if first.type == "identifier" or second.type == "identifier": if operator == "plus": repl.value = first.value + second.value return repl elif operator == "eq": return Util.castNativeToNode(first.value == second.value) elif operator == "ne": return Util.castNativeToNode(first.value != second.value) else: raise OperationError("Unsupported string/identifier operation", node) else: if operator == "plus": repl.value = str(first.value) + str(second.value) return repl else: raise OperationError("Unsupported string operation", node) # Just handle when not both are null - equal condition is already done before elif first.type == "null" or second.type == "null": if operator == "eq": return Util.castNativeToNode(False) elif operator == "ne": return Util.castNativeToNode(True) elif operator in Util.MATH_OPERATORS: return Util.castNativeToNode(None) else: raise OperationError("Unsupported operation on null type", node) else: raise OperationError("Different types in operation: %s vs %s" % (first.type, second.type), node)