diff --git a/quixote/html/__init__.py b/quixote/html/__init__.py index fc55fee..5a730b3 100644 --- a/quixote/html/__init__.py +++ b/quixote/html/__init__.py @@ -48,6 +48,8 @@ except ImportError: from quixote.html._py_htmltext import htmltext, htmlescape, \ stringify, TemplateIO +from quixote.html._py_htmltext import _wraparg + ValuelessAttr = object() # magic singleton object def htmltag(tag, xml_end=False, css_class=None, **attrs): @@ -106,6 +108,34 @@ def url_quote(value, fallback=None): return fallback return urllib.parse.quote(stringify(value)) +def _q_join(*args): + # Used by f-strings to join the {..} parts + return htmltext('').join(args) + +def _q_format(value, conversion=-1, format_spec=None): + # Used by f-strings to format the {..} parts + if conversion == -1 and format_spec is None: + return htmlescape(value) # simple and fast case + if conversion == -1: + fmt = '{%s}' + else: + conversion = chr(conversion) + if conversion == 'r': + fmt = '{%s!r}' + elif conversion == 's': + fmt = '{%s!s}' + elif conversion == 'a': + fmt = '{%s!a}' + else: + assert 0, 'invalid conversion %r' % conversion + arg = _wraparg(value) + if format_spec: + fmt = fmt % (':' + str(format_spec)) + else: + fmt = fmt % '' + return htmltext(fmt.format(arg)) + + _saved = None def use_qpy(): """ diff --git a/quixote/ptl/ptl_parse.py b/quixote/ptl/ptl_parse.py index af74636..be63916 100644 --- a/quixote/ptl/ptl_parse.py +++ b/quixote/ptl/ptl_parse.py @@ -35,7 +35,12 @@ class TemplateTransformer(ast.NodeTransformer): names=[ast.alias(name='TemplateIO', asname='_q_TemplateIO'), ast.alias(name='htmltext', - asname='_q_htmltext')], + asname='_q_htmltext'), + ast.alias(name='_q_join', + asname='_q_join'), + ast.alias(name='_q_format', + asname='_q_format'), + ], level=0) ast.fix_missing_locations(html_imp) vars_imp = ast.ImportFrom(module='builtins', @@ -116,6 +121,46 @@ class TemplateTransformer(ast.NodeTransformer): else: return node + def visit_JoinedStr(self, node): + # JoinedStr is used for combining the parts of an f-string. + # In CPython, it is done with the BUILD_STRING opcode. We + # call quixote.html._q_join() instead + node = self.generic_visit(node) + if "html" == self._get_template_type(): + n = ast.Name(id='_q_join', ctx=ast.Load()) + n = ast.Call(func=n, args=node.values, keywords=[], + starargs=None, + kwargs=None) + ast.copy_location(n, node) + ast.fix_missing_locations(n) + return n + else: + return node + + def visit_FormattedValue(self, node): + # FormattedValue is used for the {..} parts of an f-string. + # In CPython, there is a FORMAT_VALUE opcode. We call + # quixote.html._q_format instead. + node = self.generic_visit(node) + if "html" == self._get_template_type(): + n = ast.Name(id='_q_format', ctx=ast.Load()) + conversion = ast.copy_location(ast.Num(node.conversion), node) + args = [node.value] + if node.format_spec is not None: + args += [conversion, node.format_spec] + elif node.conversion != -1: + args += [conversion] + n = ast.Call(func=n, args=args, + keywords=[], + starargs=None, + kwargs=None) + ast.copy_location(n, node) + ast.fix_missing_locations(n) + return n + else: + return node + + _template_re = re.compile(r''' ^(?P[ \t]*) def (?:[ \t]+) (?P[a-zA-Z_][a-zA-Z_0-9]*)