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]*)