def test_literal(): literal = Literal("value", True, "prefixed") assert literal.value == "value" assert literal.string == "value" assert literal.css is True assert literal.prefixed == "prefixed" assert literal.node_name == "literal" clone = literal.clone() assert clone == literal
def handle(node): # returns `block` literal if mixin's block # is used as part of a property value if node.node_name == "block": literal = Literal("block") literal.lineno = node.lineno literal.column = node.column return literal else: return node
def test_block_append(): block = Block( Comment("comment", False, False), Block(String("hello"), String("There!")), ) block.append(Literal("Literal")) block.append(true) assert block.nodes == [Literal("Literal"), true] assert block.has_properties() is False assert block.has_media() is False assert block.is_empty() is False
def test_lexer_boolean_unicode(): lexer = Lexer("if true:\n return U+abcdef;\n", {}) tokens = [token for token in lexer] assert tokens[1] == Token("boolean", true, "", lineno=1, column=4) assert tokens[4] == Token( "literal", Literal("U+abcdef"), lineno=2, column=17 )
def _mixin(self, items, dest, block): for item in items: checked = False media_passed = False if item.node_name == "returnnode": return elif item.node_name == "block": checked = True self._mixin(item.nodes, dest, block) elif item.node_name == "media": # fix link to the parent block parent_node = item.block.parent.node if parent_node and parent_node.node_name != "call": item.block.parent = block media_passed = True if media_passed or item.node_name == "property": value = None if hasattr(item, "expr"): value = item.expr # prevent `block` mixin recursion if (hasattr(item, "literal") and item.literal and hasattr(value, "first") and value.first().node_name == "block"): value = unwrap(value) value.nodes[0] = Literal( "block", lineno=self.parser.lineno, column=self.parser.column, ) if not checked: dest.append(item)
def contrast(top: Color = None, bottom: Color = None, evaluator=None): if not isinstance(top, Color) and not isinstance(bottom, Color): c = "" if isinstance(top, (Null, type(None))) else f"{top}" return Literal(f"contrast({c})") result = ObjectNode() if not bottom: bottom = RGBA(255, 255, 255, 1) assert_color(bottom) bottom = bottom.rgba() def contrast_function(top, bottom): if 1 > top.a: top = blend(top, bottom) l1 = luminosity(bottom).value + 0.05 l2 = luminosity(top).value + 0.05 ratio = l1 / l2 if l2 > l1: ratio = 1 / ratio return round(ratio * 10) / 10 if 1 <= bottom.a: result_ratio = Unit(contrast_function(top, bottom)) result.set("ratio", result_ratio) result.set("error", Unit(0)) result.set("min", result_ratio) result.set("max", result_ratio) else: on_black = contrast_function(top, blend(bottom, RGBA(0, 0, 0, 1))) on_white = contrast_function(top, blend(bottom, RGBA(255, 255, 255, 1))) the_max = max(on_black, on_white) def process_channel(top_channel, bottm_channel): return min( max( 0, (top_channel - bottm_channel * bottom.a) / (1 - bottom.a), ), 255, ) closest = RGBA( process_channel(top.r, bottom.r), process_channel(top.g, bottom.g), process_channel(top.b, bottom.b), 1, ) the_min = contrast_function(top, blend(bottom, closest)) result.set("ratio", Unit(stilus_round((the_min + the_max) * 50) / 100)) result.set("error", Unit(stilus_round((the_max - the_min) * 50) / 100)) result.set("min", Unit(the_min)) result.set("max", Unit(the_max)) return result
def test_lexer_urlchars_important(): lexer = Lexer('url("/images/foo.png")\n' "!important foo", {}) tokens = [token for token in lexer] assert tokens[1] == Token( "string", String("/images/foo.png", '"'), lineno=1, column=5 ) assert tokens[4] == Token( "ident", Literal("!important"), lineno=2, column=1 )
def opposite_position(positions, evaluator=None): expr = [] nodes = utils.unwrap(positions) for i, node in enumerate(nodes): utils.assert_string(node, f"position {i}") if node.string == "top": expr.append(Literal("bottom")) elif node.string == "bottom": expr.append(Literal("top")) elif node.string == "left": expr.append(Literal("right")) elif node.string == "right": expr.append(Literal("left")) elif node.string == "center": expr.append(Literal("center")) else: raise StilusError(f"invalid position {i}") return expr
def fn(url, enc=None): compiler = Compiler(url, {}) url = compiler.visit(url) # parse the url url = urlparse(url[1:-1]) ext = Path(url.path).suffix mime = None if ext and ext in mimes: mime = mimes[ext] hash = "" if url.fragment: hash = f"#{url.fragment}" literal = Literal(f'url("{url.geturl()}")') # not mime or absolute if not mime or url.scheme: return literal # lookup found = utils.lookup(url.path, _paths) if not found: # todo: add event management logging.warning(f"File not found; File {literal} could not be " f"found, literal url retained!") return literal # read the url as a binary buf = open(found, "rb").read() # too large? if size_limit and len(buf) > size_limit: return literal if enc and enc.first().value.lower() == "utf8": encoding = "charset=utf-8" buf = re.sub(r"\s+", " ", buf.decode("utf-8")) result = urllib.parse.quote(buf, safe=" ?=:/").strip() else: encoding = "base64" result = f'{base64.b64encode(buf).decode("utf-8")}{hash}' return Literal(f'url("data:{mime};{encoding},{result}")')
def replace(node): if (node.node_name == "call" and hasattr(node, "function_name") and name == node.function_name): return Literal( "__CALL__", lineno=self.parser.lineno, column=self.parser.column, ) if hasattr(node, "nodes") and node.nodes: node.nodes = [replace(n) for n in node.nodes] return node
def bubble(self, node: Node): props = [] others = [] def filter_props(block): for node in block.nodes: node = self.visit(node) if node.node_name == "property": props.append(node) elif node.node_name == "block": filter_props(node) else: others.append(node) filter_props(node.block) if props: selector = Selector([Literal("&")]) selector.lineno = node.lineno selector.column = node.column selector.filename = node.filename selector.value = "&" group = Group(lineno=self.parser.lineno, column=self.parser.column) group.lineno = node.lineno group.column = node.column group.filename = node.filename block = Block(node.block, group) block.lineno = node.lineno block.column = node.column block.filename = node.filename for prop in props: block.append(prop) group.append(selector) group.block = block node.block.nodes = [] node.block.append(group) for other in others: node.block.append(other) group = self.closest_group(node.block) if group: node.group = group.clone() node.bubbled = True
def test_parser_parse_basic(): parser = Parser("abc\n color: red\n", {}) root = parser.parse() assert root.node_name == "root" assert root.nodes[0].node_name == "group" selector = root.nodes[0].nodes[0] assert selector.segments[0] == Literal("abc") assert selector.block.parent == root assert selector.block.node.node_name == "group" property = selector.block.nodes[0] assert property.node_name == "property" assert len(property.segments) == 1 assert property.segments[0] == Ident("color", null, lineno=2, column=3) assert len(property.expr.nodes) == 1 assert property.expr.nodes[0] == Ident("red", null, lineno=2, column=10)
def s(fmt, *args, evaluator=None): options = evaluator.options if evaluator else {} fmt = unwrap(fmt).nodes[0] assert_string(fmt, "string") result = fmt.string results = [] for arg in args: from stilus.visitor.evaluator import Evaluator if not isinstance(arg, Evaluator): results.append(Compiler(arg, options).compile()) for r in results: result = result.replace("%s", r, 1) # add nulls for missed %s elements c = Compiler(null, options).compile() result = result.replace("%s", c) return Literal(result)
def test_parser_selector(): parser = Parser("abc\n color: red\n", {}) selector = parser.stmt_selector() assert selector.node_name == "group" assert type(selector) == Group assert len(selector.nodes) == 1 assert selector.nodes[0].node_name == "selector" assert len(selector.nodes[0].segments) == 1 assert selector.nodes[0].segments[0] == Literal("abc") block = selector.nodes[0].block assert block.node_name == "block" assert len(block.nodes) == 1 property = block.nodes[0] assert property.node_name == "property" assert len(property.segments) == 1 assert property.segments[0] == Ident("color", null, lineno=2, column=3) assert len(property.expr.nodes) == 1 assert property.expr.nodes[0] == Ident("red", null, lineno=2, column=10)
def base_convert(num, base, width=None, evaluator=None): if isinstance(num, Expression): num = unwrap(num).nodes[0] if isinstance(base, Expression): base = unwrap(base).nodes[0] if width and isinstance(width, Expression): width = unwrap(width).nodes[0] assert_type(num, "unit") assert_type(base, "unit") if width: assert_type(width, "unit") if width: width = width.value else: width = 2 num = int(num.value) base = int(base.value) result = _convert(num, base) while len(result) < width: result = "0" + result return Literal(result)
def visit_group(self, group): stack = self.stack normalized = [] # normalize interpolated selectors with a comma for selector in group.nodes: if selector.value: # do nothing if "," not in selector.value: normalized = group.nodes break # replace '\,' with ',' if "\\" in selector.value: selector.value = re.sub(r"\\,", ",", selector.value) normalized = group.nodes break parts = selector.value.split(",") root = True if selector.value[0] == "/" else False for i, part in enumerate(parts): part = part.strip() if root and "&" not in part: part = "/" + part s = Selector([Literal(part)]) s.value = part s.block = group.block normalized.append(s) group.nodes = normalized stack.append(normalized) selectors = utils.compile_selectors(stack, True) # add selectors for lookup for selector in selectors: self.selector_map[selector].append(group) # extensions self.extend(group, selectors) stack.pop() return group
def visit_call(self, call): fn = self.lookup(call.function_name) log.debug(f"Visiting {fn}; {call}...") if hasattr(fn, "param") and fn.param: log.debug(fn.params) # url() self.ignore_colors = "url" == call.function_name # variable function if fn and "expression" == fn.node_name: fn = fn.nodes[0] # not a function? try user-defined or built-ins if fn and "function" != fn.node_name: fn = self.lookup_function(call.function_name) # undefined function? render literal css if fn is None or fn.node_name != "function": ret = None if "calc" == self.unvendorize(call.function_name): if call.args.nodes: if call.args.nodes[0]: ret = Literal( call.function_name + str(call.args.nodes[0]), lineno=self.parser.lineno, column=self.parser.column, ) else: ret = self.literal_call(call) self.ignore_colors = False return ret self.calling.append(call.function_name) # massive stack if len(self.calling) > 200: raise ParseError("Maximum stylus call stack size exceeded") # first node in expression if fn and "expression" == fn.function_name: fn = fn.first() # evaluate arguments self.result += 1 args = self.visit(call.args) if hasattr(args, "map"): for key in args.map: args.map[key] = self.visit(args.map[key].clone()) self.result -= 1 log.debug(f"Going to invoke function: {fn} " f"(builtin: {fn.builtin}).") if fn.builtin or (fn.function_name == "url" and hasattr(fn, "params") and fn.params.__name__ == "resolver"): log.debug(f"{fn} is a built-in method ({fn.params}).") ret = self.invoke_builtin(fn.params, args) elif "function" == fn.node_name: # user-defined # evaluate mixin block log.debug(f"{fn} is a regular method.") if call.block: log.debug(f"{fn} is a mixin.") call.block = self.visit(call.block) ret = self.invoke_function(fn, args, call.block) self.calling.pop() self.ignore_colors = False return ret
def test_parser_selector_parts(): parser = Parser("abc\n color: red\n", {}) assert parser.selector_parts() == deque([Literal("abc")]) parser = Parser("abc def efg\n color: red\n", {}) assert parser.selector_parts() == deque( [ Literal("abc"), Literal(" "), Literal("def"), Literal(" "), Literal("efg"), ] ) parser = Parser("abc:\n color: red\n", {}) assert parser.selector_parts() == deque( [ Literal("abc"), Literal(":"), Literal("color"), Literal(":"), Literal(" "), Literal("red"), ] )
def test_operate(): literal_1 = Literal("foo") literal_2 = Literal("bar") assert literal_1.operate("+", literal_2).string == "foobar"
def test_lexer_escaped(): lexer = Lexer("bar: 1 \\+ 2\n", {}) tokens = [token for token in lexer] assert tokens[3] == Token("ident", Literal("+"), lineno=1, column=8)
def test_hash(): literal = Literal("foo") assert literal.hash() == "foo"
def unquote(string, evaluator=None): assert_string(string, "string") return Literal(string.string)
def invoke_function(self, fn, args, content): block = Block( fn.block.parent, None, lineno=self.parser.lineno, column=self.parser.column, ) # clone the function body to prevent mutation of subsequent calls body = fn.block.clone(block) mixin_block = self.stack.current_frame().block # new block scope self.stack.append(Frame(block)) scope = self.get_current_scope() # normalize arguments if args.node_name != "arguments": expr = Expression() expr.append(args) args = Arguments.from_expression(expr) # arguments scope.add(Ident("arguments", args)) # mixin scope introspection bn = mixin_block.node_name if self.result: scope.add(Ident("mixin", false)) else: scope.add( Ident( "mixin", String( bn, lineno=self.parser.lineno, column=self.parser.column, ), )) # current property if self.property: prop = self.property_expression(self.property, fn.function_name) scope.add( Ident( "current-property", prop, lineno=self.parser.lineno, column=self.parser.column, )) else: scope.add( Ident( "current-property", null, lineno=self.parser.lineno, column=self.parser.column, )) # current call stack expr = Expression(lineno=self.parser.lineno, column=self.parser.column) for call in reversed(self.calling[:-1]): expr.append( Literal(call, lineno=self.parser.lineno, column=self.parser.column)) scope.add( Ident( "called-from", expr, lineno=self.parser.lineno, column=self.parser.column, )) # inject arguments as locals # todo: rewrite this! i = 0 for index, node in enumerate(fn.params.nodes): # rest param support if node.rest: node.value = Expression(lineno=self.parser.lineno, column=self.parser.column) for n in args.nodes[i:]: node.value.append(n) node.value.preserve = True node.value.is_list = args.is_list else: # argument default support # in dict? arg = args.map.get(node.name) # next node? if not arg: if hasattr(args, "nodes") and i < len(args.nodes): arg = args.nodes[i] i += 1 node = node.clone() if arg is not None: if hasattr(arg, "is_empty") and arg.is_empty(): args.nodes[i - 1] = self.visit(node) else: node.value = arg else: args.append(node.value) # required argument not satisfied if not node.value: raise TypeError(f'argument "{node}" required for {fn}') scope.add(node) # mixin block if content: scope.add(Ident("block", content, True)) # invoke return self.invoke(body, True, fn.filename)
def resolver(url, options=None, evaluator=None): frame = inspect.stack()[1] module = inspect.getmodule(frame[0]) log.debug(f"Resolver called by {module.__file__} " f"({frame[0].f_lineno}).") log.debug(f'Functions: {evaluator.options["functions"]}.') if not options: options = {} original = url # super ugly # compile the urls nodes and create a url from the result compiler = Compiler(url, options) filename = url.filename compiler.is_url = True url = urlparse(compiler.visit(url)) # fixme: dirty hack if url.geturl() == "" and f"{original}" in ["'#'", "('#')"]: literal = Literal('url("#")') else: # regular call literal = Literal(f'url("{url.geturl()}")') path = url.path dest = options.get("dest", "") tail = "" # absolute or hash if url.scheme or not path or "/" == path[0]: return literal # check that a file exists if options.get("nocheck", True): _paths = options.get("paths", []) _paths.extend(evaluator.paths) path = utils.lookup(path, _paths) if not path: return literal else: path = Path(path) if evaluator.include_css and path.suffix == ".css": return Literal(url.geturl()) if url.query or url.fragment: # fixme: extend url with a sep! if "#" in f"{original}": tail += "#" else: tail += "?" if url.query: tail += url.query if url.fragment: tail += url.fragment if dest and dest.suffix == ".css": dest = dest.parent if dest: first = dest.parents[1] else: first = Path(evaluator.filename).parent if options.get("nocheck", False): other = Path(filename).parent else: other = path res = other.relative_to(first.resolve()) # use the first path of the options['paths'] list as cwd cwd = Path(evaluator.options.get("paths", ["."])[0]) try: res = f"{res.resolve().relative_to(cwd)}{tail}" except ValueError: res = f"{res}{tail}" # todo: handle windows separators? return Literal(f'url("{res}")')
def import_file(self, node: Import, file, literal, lineno=1, column=1): log.debug(f"importing {file}; {self.import_stack}") # handling 'require' if node.once: if self.require_history.get(file, False): return null self.require_history[file] = True if literal and not self.include_css: return node # avoid overflows from reimporting the same file if file in self.import_stack: raise ImportError("import loop has been found") with open(file, "r") as f: source = f.read() # shortcut for empty files if not source.strip(): return null # expose imports node.path = file node.dirname = Path(file).parent # store modified time node.mtime = os.stat(file).st_mtime self.paths.append(str(node.dirname)) if "_imports" in self.options: self.options["_imports"].append(node.clone()) # parse the file self.import_stack.append(file) filename_node.current_filename = file if literal: re.sub("\n\n?", "\n", source) literal = Literal(source, lineno=self.parser.lineno, column=self.parser.column) literal.lineno = 1 literal.column = 1 if not self.resolve_url: return literal # create block block = Block(None, None, lineno=lineno, column=column) block.filename = file # parse merged = {} merged.update(self.options) merged.update({"root": block}) parser = Parser(source, merged) try: block = parser.parse() except Exception: line = parser.lexer.lineno column = parser.lexer.column if literal and self.include_css and self.resolve_url: self.warn(f"ParseError: {file}:{line}:{column}. " f"This file is included as-is") return literal else: raise ParseError( "Issue when parsing an imported file", filename=file, lineno=line, column=column, input=source, ) # evaluate imported 'root' block = block.clone(self.get_current_block()) block.parent = self.get_current_block() block.scope = False ret = self.visit(block) self.import_stack.pop() if not self.resolve_url: # or not self.resolve_url no_check: self.paths.pop() return ret
def test_coerce(): literal_1 = Literal("first") literal_2 = Literal("second") assert literal_1.coerce(literal_2).string == "second"