mirror of
https://github.com/explosion/spaCy.git
synced 2024-11-11 12:18:04 +03:00
e2f2ef3a5a
- As much as I dislike YAML, it seemed like a better format here because it allows us to add comments if we want to explain the different recommendations - Don't include the generated JS in the repo by default and build it on the fly when running or deploying the site. This ensures it's always up to date. - Simplify jinja_to_js script and use fewer dependencies
1274 lines
46 KiB
Python
1274 lines
46 KiB
Python
# Forked from: https://github.com/jonbretman/jinja-to-js
|
|
# With additional functionality: in/not in, replace, pprint, round, + for lists,
|
|
# rendering empty dicts
|
|
# This script is mostly used to generate the JavaScript function for the
|
|
# training quickstart widget.
|
|
import contextlib
|
|
import json
|
|
import re
|
|
import os
|
|
from os import path
|
|
from io import StringIO
|
|
from jinja2 import Environment, FileSystemLoader, nodes
|
|
from pathlib import Path
|
|
import srsly
|
|
import sys
|
|
|
|
|
|
OPERANDS = {
|
|
"eq": "===",
|
|
"ne": "!==",
|
|
"lt": " < ",
|
|
"gt": " > ",
|
|
"lteq": " <= ",
|
|
"gteq": " >= ",
|
|
}
|
|
|
|
DICT_ITER_METHODS = ("iteritems", "items", "values", "keys")
|
|
|
|
STATE_DEFAULT = 0
|
|
STATE_EXECUTING = 1
|
|
STATE_INTERPOLATING = 2
|
|
|
|
LOOP_HELPER_INDEX = "index"
|
|
LOOP_HELPER_INDEX_0 = "index0"
|
|
LOOP_HELPER_FIRST = "first"
|
|
LOOP_HELPER_LAST = "last"
|
|
LOOP_HELPER_LENGTH = "length"
|
|
LOOP_HELPERS = (
|
|
LOOP_HELPER_INDEX,
|
|
LOOP_HELPER_INDEX_0,
|
|
LOOP_HELPER_FIRST,
|
|
LOOP_HELPER_LAST,
|
|
LOOP_HELPER_LENGTH,
|
|
)
|
|
|
|
|
|
def amd_format(dependencies, template_function):
|
|
result = "define(["
|
|
result += ",".join('"{0}"'.format(x[0]) for x in dependencies)
|
|
result += "], function ("
|
|
result += ",".join(x[1] for x in dependencies)
|
|
result += ") { return "
|
|
result += template_function
|
|
result += "; });"
|
|
return result
|
|
|
|
|
|
def commonjs_format(dependencies, template_function):
|
|
result = "".join('var {0} = require("{1}");'.format(y, x) for x, y in dependencies)
|
|
result += "module.exports = {0};".format(template_function)
|
|
return result
|
|
|
|
|
|
def es6_format(dependencies, template_function):
|
|
result = "".join('import {0} from "{1}";'.format(y, x) for x, y in dependencies)
|
|
result += "export default {0}".format(template_function)
|
|
return result
|
|
|
|
|
|
JS_MODULE_FORMATS = {
|
|
None: lambda dependencies, template_function: template_function,
|
|
"amd": amd_format,
|
|
"commonjs": commonjs_format,
|
|
"es6": es6_format,
|
|
}
|
|
|
|
|
|
# This string has to double all the '{' and '}' due to Python's string formatting.
|
|
# See - https://docs.python.org/2/library/string.html#formatstrings
|
|
TEMPLATE_WRAPPER = """
|
|
function {function_name}(ctx) {{
|
|
var __result = "";
|
|
var __tmp;
|
|
var __runtime = jinjaToJS.runtime;
|
|
var __filters = jinjaToJS.filters;
|
|
var __globals = jinjaToJS.globals;
|
|
var context = jinjaToJS.createContext(ctx);
|
|
{template_code}
|
|
return __result;
|
|
}}
|
|
"""
|
|
|
|
|
|
class ExtendsException(Exception):
|
|
"""
|
|
Raised when an {% extends %} is encountered. At this point the parent template is
|
|
loaded and all blocks defined in the current template passed to it.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def option(current_kwargs, **kwargs):
|
|
"""
|
|
Context manager for temporarily setting a keyword argument and
|
|
then restoring it to whatever it was before.
|
|
"""
|
|
|
|
tmp_kwargs = dict((key, current_kwargs.get(key)) for key, value in kwargs.items())
|
|
current_kwargs.update(kwargs)
|
|
yield
|
|
current_kwargs.update(tmp_kwargs)
|
|
|
|
|
|
def is_method_call(node, method_name):
|
|
"""
|
|
Returns True if `node` is a method call for `method_name`. `method_name`
|
|
can be either a string or an iterable of strings.
|
|
"""
|
|
|
|
if not isinstance(node, nodes.Call):
|
|
return False
|
|
|
|
if isinstance(node.node, nodes.Getattr):
|
|
# e.g. foo.bar()
|
|
method = node.node.attr
|
|
|
|
elif isinstance(node.node, nodes.Name):
|
|
# e.g. bar()
|
|
method = node.node.name
|
|
|
|
elif isinstance(node.node, nodes.Getitem):
|
|
# e.g. foo["bar"]()
|
|
method = node.node.arg.value
|
|
|
|
else:
|
|
return False
|
|
|
|
if isinstance(method_name, (list, tuple)):
|
|
return method in method_name
|
|
|
|
return method == method_name
|
|
|
|
|
|
def is_loop_helper(node):
|
|
"""
|
|
Returns True is node is a loop helper e.g. {{ loop.index }} or {{ loop.first }}
|
|
"""
|
|
return (
|
|
hasattr(node, "node")
|
|
and isinstance(node.node, nodes.Name)
|
|
and node.node.name == "loop"
|
|
)
|
|
|
|
|
|
def temp_var_names_generator():
|
|
x = 0
|
|
while True:
|
|
yield "__$%s" % x
|
|
x += 1
|
|
|
|
|
|
class JinjaToJS(object):
|
|
def __init__(
|
|
self,
|
|
template_root,
|
|
template_name,
|
|
js_module_format=None,
|
|
runtime_path="jinja-to-js",
|
|
include_prefix="",
|
|
include_ext="",
|
|
child_blocks=None,
|
|
dependencies=None,
|
|
custom_filters=None,
|
|
):
|
|
"""
|
|
Args:
|
|
template_root (str): The path to where templates should be loaded from.
|
|
template_name (str): The name of the template to compile (relative to `template_root`).
|
|
js_module_format (str, optional): The JavaScript module format to use.
|
|
One of ('amd', 'commonjs', 'es6')
|
|
runtime_path (str, optional): If `js_module_format` is specified then the JavaScript
|
|
runtime will be imported using the appropriate method.
|
|
It defaults to assuming it will be imported from
|
|
`node_modules` but you can change it using this option.
|
|
include_prefix (str, optional): If using the `amd` module format you can use this option
|
|
to add a prefix to every include path as AMD imports are
|
|
generally relative to the main file, not the module
|
|
importing.
|
|
include_ext (str, optional): By default any includes will be references without an
|
|
extension, as neither AMD, commonJS or ES6 require the
|
|
'.js' extension. If you want to use an extension, say
|
|
'.template' then set this option to a string including
|
|
the leading '.'
|
|
child_blocks (dict, optional): Used internally when handling templates that extend
|
|
other templates.
|
|
dependencies (list of tuple, optional): Used internally when handling templates that
|
|
extend other templates.
|
|
custom_filters (list of str, optional): List of custom filters which should be allowed.
|
|
These may be filters supported by Jinja but not
|
|
supported by jinja-to-js. These filters MUST be
|
|
registered with the jinja-to-js JS runtime.
|
|
"""
|
|
|
|
self.environment = Environment(
|
|
loader=FileSystemLoader(template_root),
|
|
autoescape=True,
|
|
extensions=["jinja2.ext.with_", "jinja2.ext.autoescape"],
|
|
)
|
|
self.output = StringIO()
|
|
self.stored_names = set()
|
|
self.temp_var_names = temp_var_names_generator()
|
|
self.state = STATE_DEFAULT
|
|
self.child_blocks = child_blocks or {}
|
|
self.dependencies = dependencies or []
|
|
self._runtime_function_cache = []
|
|
self.js_module_format = js_module_format
|
|
self.runtime_path = runtime_path
|
|
self.include_prefix = include_prefix
|
|
self.include_ext = include_ext
|
|
self.template_root = template_root
|
|
self.template_name = template_name
|
|
self.custom_filters = custom_filters or []
|
|
|
|
# The name of the JavaScript function that will output this template. By using a named
|
|
# function the template can call itself which is required to support recursive includes.
|
|
self.js_function_name = "template" + "".join(
|
|
x.title()
|
|
for x in re.split(r"[^\w]|_", path.splitext(self.template_name)[0])
|
|
)
|
|
|
|
self.context_name = "context"
|
|
|
|
self._add_dependency(self.runtime_path, "jinjaToJS")
|
|
|
|
# Jinja2 doesn't accept Windows filepaths
|
|
if os.name == "nt":
|
|
self.template_name = self.template_name.replace(os.pathsep, "/")
|
|
|
|
template_string, template_path, _ = self.environment.loader.get_source(
|
|
self.environment, self.template_name
|
|
)
|
|
|
|
# It is assumed that this will be the absolute path to the template. It is used to work out
|
|
# related paths for inclues.
|
|
self.template_path = template_path
|
|
|
|
if self.js_module_format not in JS_MODULE_FORMATS.keys():
|
|
raise ValueError(
|
|
"The js_module_format option must be one of: %s"
|
|
% JS_MODULE_FORMATS.keys()
|
|
)
|
|
|
|
self.ast = self.environment.parse(template_string)
|
|
|
|
try:
|
|
for node in self.ast.body:
|
|
self._process_node(node)
|
|
except ExtendsException:
|
|
pass
|
|
|
|
def get_output(self):
|
|
"""
|
|
Returns the generated JavaScript code.
|
|
|
|
Returns:
|
|
str
|
|
"""
|
|
# generate the JS function string
|
|
template_function = TEMPLATE_WRAPPER.format(
|
|
function_name=self.js_function_name, template_code=self.output.getvalue()
|
|
).strip()
|
|
|
|
# get the correct module format template
|
|
module_format = JS_MODULE_FORMATS[self.js_module_format]
|
|
|
|
# generate the module code
|
|
return module_format(self.dependencies, template_function)
|
|
|
|
def _get_depencency_var_name(self, dependency):
|
|
"""
|
|
Returns the variable name assigned to the given dependency or None if the dependency has
|
|
not yet been registered.
|
|
|
|
Args:
|
|
dependency (str): Thet dependency that needs to be imported.
|
|
|
|
Returns:
|
|
str or None
|
|
"""
|
|
for dep_path, var_name in self.dependencies:
|
|
if dep_path == dependency:
|
|
return var_name
|
|
|
|
def _add_dependency(self, dependency, var_name=None):
|
|
"""
|
|
Adds the given dependency and returns the variable name to use to access it. If `var_name`
|
|
is not given then a random one will be created.
|
|
|
|
Args:
|
|
dependency (str):
|
|
var_name (str, optional):
|
|
|
|
Returns:
|
|
str
|
|
"""
|
|
if var_name is None:
|
|
var_name = next(self.temp_var_names)
|
|
# Don't add duplicate dependencies
|
|
if (dependency, var_name) not in self.dependencies:
|
|
self.dependencies.append((dependency, var_name))
|
|
return var_name
|
|
|
|
def _process_node(self, node, **kwargs):
|
|
node_name = node.__class__.__name__.lower()
|
|
handler = getattr(self, "_process_" + node_name, None)
|
|
if callable(handler):
|
|
handler(node, **kwargs)
|
|
else:
|
|
raise Exception(f"Unknown node {node} ({node_name})")
|
|
|
|
def _process_extends(self, node, **kwargs):
|
|
"""
|
|
Processes an extends block e.g. `{% extends "some/template.jinja" %}`
|
|
"""
|
|
|
|
# find all the blocks in this template
|
|
for b in self.ast.find_all(nodes.Block):
|
|
|
|
# if not already in `child_blocks` then this is the first time a
|
|
# block with this name has been encountered.
|
|
if b.name not in self.child_blocks:
|
|
self.child_blocks[b.name] = b
|
|
else:
|
|
|
|
# otherwise we have seen this block before, so we need to find the last
|
|
# super_block and add the block from this template to the end.
|
|
block = self.child_blocks.get(b.name)
|
|
while hasattr(block, "super_block"):
|
|
block = block.super_block
|
|
block.super_block = b
|
|
|
|
# load the parent template
|
|
parent_template = JinjaToJS(
|
|
template_root=self.template_root,
|
|
template_name=node.template.value,
|
|
js_module_format=self.js_module_format,
|
|
runtime_path=self.runtime_path,
|
|
include_prefix=self.include_prefix,
|
|
include_ext=self.include_ext,
|
|
child_blocks=self.child_blocks,
|
|
dependencies=self.dependencies,
|
|
)
|
|
|
|
# add the parent templates output to the current output
|
|
self.output.write(parent_template.output.getvalue())
|
|
|
|
# Raise an exception so we stop parsing this template
|
|
raise ExtendsException
|
|
|
|
def _process_block(self, node, **kwargs):
|
|
"""
|
|
Processes a block e.g. `{% block my_block %}{% endblock %}`
|
|
"""
|
|
|
|
# check if this node already has a 'super_block' attribute
|
|
if not hasattr(node, "super_block"):
|
|
|
|
# since it doesn't it must be the last block in the inheritance chain
|
|
node.super_block = None
|
|
|
|
# see if there has been a child block defined - if there is this
|
|
# will be the first block in the inheritance chain
|
|
child_block = self.child_blocks.get(node.name)
|
|
|
|
if child_block:
|
|
|
|
# we have child nodes so we need to set `node` as the
|
|
# super of the last one in the chain
|
|
last_block = child_block
|
|
while hasattr(last_block, "super_block"):
|
|
last_block = child_block.super_block
|
|
|
|
# once we have found it, set this node as it's super block
|
|
last_block.super_block = node
|
|
|
|
# this is the node we want to process as it's the first in the inheritance chain
|
|
node = child_block
|
|
|
|
# process the block passing the it's super along, if this block
|
|
# calls super() it will be handled by `_process_call`
|
|
for n in node.body:
|
|
self._process_node(n, super_block=node.super_block, **kwargs)
|
|
|
|
def _process_output(self, node, **kwargs):
|
|
"""
|
|
Processes an output node, which will contain things like `Name` and `TemplateData` nodes.
|
|
"""
|
|
for n in node.nodes:
|
|
self._process_node(n, **kwargs)
|
|
|
|
def _process_templatedata(self, node, **_):
|
|
"""
|
|
Processes a `TemplateData` node, this is just a bit of as-is text
|
|
to be written to the output.
|
|
"""
|
|
|
|
# escape double quotes
|
|
value = re.sub('"', r'\\"', node.data)
|
|
|
|
# escape new lines
|
|
value = re.sub("\n", r"\\n", value)
|
|
|
|
# append value to the result
|
|
self.output.write('__result += "' + value + '";')
|
|
|
|
def _process_name(self, node, **kwargs):
|
|
"""
|
|
Processes a `Name` node. Some examples of `Name` nodes:
|
|
{{ foo }} -> 'foo' is a Name
|
|
{% if foo }} -> 'foo' is a Name
|
|
"""
|
|
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs):
|
|
|
|
if node.name not in self.stored_names and node.ctx != "store":
|
|
self.output.write(self.context_name)
|
|
self.output.write(".")
|
|
|
|
if node.ctx == "store":
|
|
self.stored_names.add(node.name)
|
|
|
|
self.output.write(node.name)
|
|
|
|
def _process_dict(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs):
|
|
if node.items:
|
|
err = f"Can't process non-empty dict in expression: {node}"
|
|
raise ValueError(err)
|
|
self.output.write("{}")
|
|
|
|
def _process_getattr(self, node, **kwargs):
|
|
"""
|
|
Processes a `GetAttr` node. e.g. {{ foo.bar }}
|
|
"""
|
|
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
if is_loop_helper(node):
|
|
self._process_loop_helper(node, **new_kwargs)
|
|
else:
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(".")
|
|
self.output.write(node.attr)
|
|
|
|
def _process_getitem(self, node, **kwargs):
|
|
"""
|
|
Processes a `GetItem` node e.g. {{ foo["bar"] }}
|
|
"""
|
|
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
|
|
if isinstance(node.arg, nodes.Slice):
|
|
self.output.write(".slice(")
|
|
|
|
if node.arg.step is not None:
|
|
raise Exception(
|
|
"The step argument is not supported when slicing."
|
|
)
|
|
|
|
if node.arg.start is None:
|
|
self.output.write("0")
|
|
else:
|
|
self._process_node(node.arg.start, **new_kwargs)
|
|
|
|
if node.arg.stop is None:
|
|
self.output.write(")")
|
|
else:
|
|
self.output.write(",")
|
|
self._process_node(node.arg.stop, **new_kwargs)
|
|
self.output.write(")")
|
|
else:
|
|
self.output.write("[")
|
|
self._process_node(node.arg, **new_kwargs)
|
|
self.output.write("]")
|
|
|
|
def _process_for(self, node, **kwargs):
|
|
"""
|
|
Processes a for loop. e.g.
|
|
{% for number in numbers %}
|
|
{{ number }}
|
|
{% endfor %}
|
|
{% for key, value in somemap.items() %}
|
|
{{ key }} -> {{ value }}
|
|
{% %}
|
|
"""
|
|
|
|
# since a for loop can introduce new names into the context
|
|
# we need to remember the ones that existed outside the loop
|
|
previous_stored_names = self.stored_names.copy()
|
|
|
|
with self._execution():
|
|
self.output.write("__runtime.each(")
|
|
|
|
if is_method_call(node.iter, dict.keys.__name__):
|
|
self.output.write("Object.keys(")
|
|
|
|
self._process_node(node.iter, **kwargs)
|
|
|
|
if is_method_call(node.iter, dict.keys.__name__):
|
|
self.output.write(")")
|
|
|
|
self.output.write(",")
|
|
self.output.write("function")
|
|
self.output.write("(")
|
|
|
|
# javascript iterations put the value first, then the key
|
|
if isinstance(node.target, nodes.Tuple):
|
|
if len(node.target.items) > 2:
|
|
raise Exception(
|
|
"De-structuring more than 2 items is not supported."
|
|
)
|
|
|
|
for i, item in enumerate(reversed(node.target.items)):
|
|
self._process_node(item, **kwargs)
|
|
if i < len(node.target.items) - 1:
|
|
self.output.write(",")
|
|
else:
|
|
self._process_node(node.target, **kwargs)
|
|
|
|
self.output.write(")")
|
|
self.output.write("{")
|
|
|
|
if node.test:
|
|
self.output.write("if (!(")
|
|
self._process_node(node.test, **kwargs)
|
|
self.output.write(")) { return; }")
|
|
|
|
assigns = (
|
|
node.target.items if isinstance(node.target, nodes.Tuple) else [node.target]
|
|
)
|
|
|
|
with self._scoped_variables(assigns, **kwargs):
|
|
for n in node.body:
|
|
self._process_node(n, **kwargs)
|
|
|
|
with self._execution():
|
|
self.output.write("}")
|
|
self.output.write(")")
|
|
self.output.write(";")
|
|
|
|
# restore the stored names
|
|
self.stored_names = previous_stored_names
|
|
|
|
def _process_if(self, node, execute_end=None, **kwargs):
|
|
"""
|
|
Processes an if block e.g. `{% if foo %} do something {% endif %}`
|
|
"""
|
|
|
|
with self._execution():
|
|
self.output.write("if")
|
|
self.output.write("(")
|
|
|
|
with option(kwargs, use_python_bool_wrapper=True):
|
|
self._process_node(node.test, **kwargs)
|
|
|
|
self.output.write(")")
|
|
self.output.write("{")
|
|
|
|
# We accept an `execute_end` function as a keyword argument as this function is
|
|
# recursive in the case of something like if-elif-elif-else. In these cases this
|
|
# invocation of this function may have to close execution opened by a previous
|
|
# invocation of this function.
|
|
if execute_end:
|
|
execute_end()
|
|
|
|
# body
|
|
for n in node.body:
|
|
self._process_node(n, **kwargs)
|
|
|
|
if not node.else_ and not node.elif_:
|
|
# no else - just close the if
|
|
with self._execution():
|
|
self.output.write("}")
|
|
|
|
else:
|
|
# either an else or an elif
|
|
with self._execution() as execute_end:
|
|
self.output.write("}")
|
|
self.output.write(" else ")
|
|
|
|
# check for elif
|
|
for n in node.elif_:
|
|
self._process_node(n, execute_end=execute_end, **kwargs)
|
|
|
|
if node.elif_ and node.else_:
|
|
self.output.write(" else ")
|
|
|
|
# open up the body
|
|
self.output.write("{")
|
|
|
|
# process the body of the else
|
|
for n in node.else_:
|
|
self._process_node(n, **kwargs)
|
|
|
|
# close the body
|
|
with self._execution():
|
|
self.output.write("}")
|
|
|
|
def _process_condexpr(self, node, **kwargs):
|
|
with self._interpolation():
|
|
self.output.write("(")
|
|
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.test, **new_kwargs)
|
|
|
|
self.output.write(" ? ")
|
|
self._process_node(node.expr1, **kwargs)
|
|
self.output.write(" : ")
|
|
self._process_node(node.expr2, **kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_not(self, node, **kwargs):
|
|
self.output.write("!")
|
|
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
|
|
def _process_or(self, node, **kwargs):
|
|
self._process_node(node.left, **kwargs)
|
|
self.output.write(" || ")
|
|
self._process_node(node.right, **kwargs)
|
|
|
|
def _process_and(self, node, **kwargs):
|
|
self._process_node(node.left, **kwargs)
|
|
self.output.write(" && ")
|
|
self._process_node(node.right, **kwargs)
|
|
|
|
def _process_tuple(self, node, **kwargs):
|
|
self.output.write("[")
|
|
for i, item in enumerate(node.items):
|
|
self._process_node(item, **kwargs)
|
|
if i < len(node.items) - 1:
|
|
self.output.write(",")
|
|
self.output.write("]")
|
|
|
|
def _process_call(self, node, super_block=None, **kwargs):
|
|
if is_method_call(node, DICT_ITER_METHODS):
|
|
# special case for dict methods
|
|
self._process_node(node.node.node, **kwargs)
|
|
|
|
elif is_method_call(node, "super"):
|
|
# special case for the super() method which is available inside blocks
|
|
if not super_block:
|
|
raise Exception("super() called outside of a block with a parent.")
|
|
self._process_node(super_block, **kwargs)
|
|
|
|
else:
|
|
# just a normal function call on a context variable
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write("(")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
# only output the semi-colon if we are not interpolating
|
|
if self.state != STATE_INTERPOLATING:
|
|
self.output.write("")
|
|
|
|
def _process_filter(self, node, **kwargs):
|
|
method_name = getattr(self, "_process_filter_%s" % node.name, None)
|
|
if callable(method_name):
|
|
method_name(node, **kwargs)
|
|
elif node.name in self.custom_filters:
|
|
with self._interpolation(safe=True):
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.%s(" % node.name)
|
|
self._process_node(node.node, **new_kwargs)
|
|
if getattr(node, "args", None):
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
else:
|
|
raise Exception("Unsupported filter: %s" % node.name)
|
|
|
|
def _process_filter_safe(self, node, **kwargs):
|
|
with self._interpolation(safe=True):
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
|
|
def _process_filter_capitalize(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.capitalize(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_abs(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("Math.abs(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_replace(self, node, **kwargs):
|
|
# We're getting a quoted string from Python/Jinja as the pattern to
|
|
# replace, but to replace all occurrences in JS, we typically need a
|
|
# regex, which would be annoying to convert. So we're using split/join
|
|
# instead here.
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(".split(")
|
|
self._process_node(node.args[0], **new_kwargs)
|
|
self.output.write(").join(")
|
|
self._process_node(node.args[1], **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_pprint(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("JSON.stringify(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_attr(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write("[")
|
|
self._process_node(node.args[0], **new_kwargs)
|
|
self.output.write("]")
|
|
|
|
def _process_filter_batch(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.batch(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_default(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.default(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
if node.args:
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_first(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.first(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_int(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.int(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
if node.args:
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_round(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("Math.round((")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write("+ Number.EPSILON) * 10**")
|
|
self._process_node(node.args[0], **new_kwargs)
|
|
self.output.write(") / 10**")
|
|
self._process_node(node.args[0], **new_kwargs)
|
|
|
|
def _process_filter_last(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.last(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_length(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.size(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_lower(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(' + "").toLowerCase()')
|
|
|
|
def _process_filter_slice(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.slice(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_title(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.title(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_filter_trim(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(' + "").trim()')
|
|
|
|
def _process_filter_upper(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(' + "").toUpperCase()')
|
|
|
|
def _process_filter_truncate(self, node, **kwargs):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self.output.write("__filters.truncate(")
|
|
self._process_node(node.node, **new_kwargs)
|
|
self.output.write(",")
|
|
self._process_args(node, **new_kwargs)
|
|
self.output.write(")")
|
|
|
|
def _process_assign(self, node, **kwargs):
|
|
with self._execution():
|
|
self.output.write("var ")
|
|
self._process_node(node.target, **kwargs)
|
|
self.output.write(" = ")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(";")
|
|
|
|
def _process_with(self, node, **kwargs):
|
|
|
|
# keep a copy of the stored names before the scope
|
|
previous_stored_names = self.stored_names.copy()
|
|
|
|
# assigns in the with tag
|
|
# e.g. {% with var = "something %}
|
|
assigns_in_tag = [nodes.Assign(t, v) for t, v in zip(node.targets, node.values)]
|
|
|
|
# assigns in the with body
|
|
# e.g. {% set name = 'John' %}
|
|
assigns_in_body = [x for x in node.body if isinstance(x, nodes.Assign)]
|
|
|
|
# remove assigns from the body
|
|
node.body = [x for x in node.body if not isinstance(x, nodes.Assign)]
|
|
|
|
# get a list of all the assigns in this with block
|
|
# both on the tag, and within the body of the block
|
|
all_assigns = assigns_in_tag + assigns_in_body
|
|
|
|
with self._execution():
|
|
self.output.write("(function () {")
|
|
|
|
with self._scoped_variables(all_assigns, **kwargs):
|
|
for node in node.body:
|
|
self._process_node(node, **kwargs)
|
|
|
|
with self._execution():
|
|
self.output.write("})();")
|
|
|
|
# restore previous stored names
|
|
self.stored_names = previous_stored_names
|
|
|
|
def _process_compare(self, node, **kwargs):
|
|
|
|
if len(node.ops) > 1:
|
|
raise Exception("Multiple operands are not supported.")
|
|
|
|
operand = node.ops[0]
|
|
is_equality = operand.op in ("eq", "ne")
|
|
left_hand_is_const = isinstance(node.expr, nodes.Const)
|
|
right_hand_is_const = isinstance(operand.expr, nodes.Const)
|
|
|
|
# If the operand is equality and neither the left or right hand side are constants then we
|
|
# will need to use the JavaScript deep equals function. Ideally we want to avoid using this
|
|
# as it is quite a big function.
|
|
use_is_equal_function = is_equality and not (
|
|
left_hand_is_const or right_hand_is_const
|
|
)
|
|
|
|
with option(kwargs, use_python_bool_wrapper=False):
|
|
if operand.op == "in" or operand.op == "notin":
|
|
# Special case for "in" operator
|
|
if operand.op == "notin":
|
|
self.output.write("!")
|
|
self._process_node(operand.expr, **kwargs)
|
|
self.output.write(".includes(")
|
|
self._process_node(node.expr, **kwargs)
|
|
self.output.write(")")
|
|
else:
|
|
if use_is_equal_function:
|
|
if operand.op == "ne":
|
|
self.output.write("!")
|
|
self.output.write("__runtime.isEqual(")
|
|
|
|
self._process_node(node.expr, **kwargs)
|
|
|
|
if use_is_equal_function:
|
|
self.output.write(",")
|
|
else:
|
|
self.output.write(OPERANDS.get(operand.op))
|
|
|
|
self._process_node(operand.expr, **kwargs)
|
|
|
|
if use_is_equal_function:
|
|
self.output.write(")")
|
|
|
|
def _process_operand(self, node, **kwargs):
|
|
self.output.write(OPERANDS.get(node.op))
|
|
self._process_node(node.expr, **kwargs)
|
|
|
|
def _process_const(self, node, **_):
|
|
with self._interpolation():
|
|
self.output.write(json.dumps(node.value))
|
|
|
|
def _process_nonetype(self, node, **_):
|
|
with self._interpolation():
|
|
self.output.write("null")
|
|
|
|
def _process_neg(self, node, **kwargs):
|
|
with self._interpolation():
|
|
self.output.write("-")
|
|
self._process_node(node.node, **kwargs)
|
|
|
|
def _process_list(self, node, **kwargs):
|
|
self.output.write("[")
|
|
for i, item in enumerate(node.items):
|
|
self._process_node(item, **kwargs)
|
|
if i < len(node.items) - 1:
|
|
self.output.write(",")
|
|
self.output.write("]")
|
|
|
|
def _process_test(self, node, **kwargs):
|
|
with option(kwargs, use_python_bool_wrapper=False):
|
|
method_name = getattr(self, "_process_test_%s" % node.name, None)
|
|
if callable(method_name):
|
|
method_name(node, **kwargs)
|
|
else:
|
|
raise Exception("Unsupported test: %s" % node.name)
|
|
|
|
def _process_test_defined(self, node, **kwargs):
|
|
self.output.write("(typeof ")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(' !== "undefined")')
|
|
|
|
def _process_test_undefined(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(" === undefined")
|
|
|
|
def _process_test_callable(self, node, **kwargs):
|
|
self.output.write("__runtime.type(")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(') === "Function"')
|
|
|
|
def _process_test_divisibleby(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(" % ")
|
|
self._process_node(node.args[0], **kwargs)
|
|
self.output.write(" === 0")
|
|
|
|
def _process_test_even(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(" % 2 === 0")
|
|
|
|
def _process_test_odd(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(" % 2 === 1")
|
|
|
|
def _process_test_none(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(" === null")
|
|
|
|
def _process_test_upper(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(".toUpperCase() === ")
|
|
self._process_node(node.node, **kwargs)
|
|
|
|
def _process_test_lower(self, node, **kwargs):
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(".toLowerCase() === ")
|
|
self._process_node(node.node, **kwargs)
|
|
|
|
def _process_test_string(self, node, **kwargs):
|
|
self.output.write("__runtime.type(")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(') === "String"')
|
|
|
|
def _process_test_mapping(self, node, **kwargs):
|
|
self.output.write("__runtime.type(")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(') === "Object"')
|
|
|
|
def _process_test_number(self, node, **kwargs):
|
|
self.output.write("(__runtime.type(")
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write(') === "Number" && !isNaN(')
|
|
self._process_node(node.node, **kwargs)
|
|
self.output.write("))")
|
|
|
|
def _process_include(self, node, **kwargs):
|
|
with self._interpolation(safe=True):
|
|
include_path = node.template.value
|
|
|
|
if include_path == self.template_name:
|
|
# template is including itself
|
|
include_var_name = self.js_function_name
|
|
else:
|
|
if self.include_prefix:
|
|
include_path = self.include_prefix + node.template.value
|
|
elif (
|
|
self.js_module_format in ("es6", "commonjs",) and self.template_name
|
|
):
|
|
_, absolute_include_path, _ = self.environment.loader.get_source(
|
|
self.environment, node.template.value
|
|
)
|
|
include_path = os.path.relpath(
|
|
absolute_include_path, os.path.dirname(self.template_path)
|
|
)
|
|
if not include_path.startswith("."):
|
|
include_path = "./" + include_path
|
|
|
|
# Jinja2 doesn't accept Windows filepaths (but does output them!)
|
|
if os.name == "nt":
|
|
include_path = include_path.replace(os.pathsep, "/")
|
|
|
|
include_path = path.splitext(include_path)[0] + self.include_ext
|
|
include_var_name = self._get_depencency_var_name(include_path)
|
|
|
|
if not include_var_name:
|
|
include_var_name = self._add_dependency(include_path)
|
|
|
|
if self.js_module_format is None:
|
|
self.output.write('jinjaToJS.include("')
|
|
self.output.write(include_path)
|
|
self.output.write('");')
|
|
else:
|
|
self.output.write(include_var_name)
|
|
|
|
self.output.write("(")
|
|
self.output.write(self.context_name)
|
|
self.output.write(")")
|
|
|
|
def _process_add(self, node, **kwargs):
|
|
# Handle + operator for lists, which behaves differently in JS. Currently
|
|
# only works if we have an explicit list node on either side (in which
|
|
# case we assume both are lists).
|
|
if isinstance(node.left, nodes.List) or isinstance(node.right, nodes.List):
|
|
with self._interpolation():
|
|
with self._python_bool_wrapper(**kwargs) as new_kwargs:
|
|
self._process_node(node.left, **new_kwargs)
|
|
self.output.write(".concat(")
|
|
self._process_node(node.right, **new_kwargs)
|
|
self.output.write(")")
|
|
else:
|
|
self._process_math(node, math_operator=" + ", **kwargs)
|
|
|
|
def _process_sub(self, node, **kwargs):
|
|
self._process_math(node, math_operator=" - ", **kwargs)
|
|
|
|
def _process_div(self, node, **kwargs):
|
|
self._process_math(node, math_operator=" / ", **kwargs)
|
|
|
|
def _process_floordiv(self, node, **kwargs):
|
|
self._process_math(node, math_operator=" / ", function="Math.floor", **kwargs)
|
|
|
|
def _process_mul(self, node, **kwargs):
|
|
self._process_math(node, math_operator=" * ", **kwargs)
|
|
|
|
def _process_mod(self, node, **kwargs):
|
|
self._process_math(node, math_operator=" % ", **kwargs)
|
|
|
|
def _process_math(self, node, math_operator=None, function=None, **kwargs):
|
|
"""
|
|
Processes a math node e.g. `Div`, `Sub`, `Add`, `Mul` etc...
|
|
If `function` is provided the expression is wrapped in a call to that function.
|
|
"""
|
|
|
|
with self._interpolation():
|
|
if function:
|
|
self.output.write(function)
|
|
self.output.write("(")
|
|
|
|
self._process_node(node.left, **kwargs)
|
|
self.output.write(math_operator)
|
|
self._process_node(node.right, **kwargs)
|
|
|
|
if function:
|
|
self.output.write(")")
|
|
|
|
def _process_loop_helper(self, node, **kwargs):
|
|
"""
|
|
Processes a loop helper e.g. {{ loop.first }} or {{ loop.index }}
|
|
"""
|
|
|
|
if node.attr == LOOP_HELPER_INDEX:
|
|
self.output.write("(arguments[1] + 1)")
|
|
elif node.attr == LOOP_HELPER_INDEX_0:
|
|
self.output.write("arguments[1]")
|
|
elif node.attr == LOOP_HELPER_FIRST:
|
|
self.output.write("(arguments[1] == 0)")
|
|
elif node.attr == LOOP_HELPER_LAST:
|
|
self.output.write("(arguments[1] == arguments[2].length - 1)")
|
|
elif node.attr == LOOP_HELPER_LENGTH:
|
|
self.output.write("arguments[2].length")
|
|
|
|
def _process_args(self, node, **kwargs):
|
|
args = getattr(node, "args", None)
|
|
if not args:
|
|
return
|
|
for i, item in enumerate(args):
|
|
self._process_node(item, **kwargs)
|
|
if i < len(node.args) - 1:
|
|
self.output.write(",")
|
|
|
|
@contextlib.contextmanager
|
|
def _execution(self):
|
|
"""
|
|
Context manager for executing some JavaScript inside a template.
|
|
"""
|
|
|
|
did_start_executing = False
|
|
|
|
if self.state == STATE_DEFAULT:
|
|
did_start_executing = True
|
|
self.state = STATE_EXECUTING
|
|
|
|
def close():
|
|
if did_start_executing and self.state == STATE_EXECUTING:
|
|
self.state = STATE_DEFAULT
|
|
|
|
yield close
|
|
close()
|
|
|
|
@contextlib.contextmanager
|
|
def _interpolation(self, safe=False):
|
|
|
|
did_start_interpolating = False
|
|
|
|
if self.state == STATE_DEFAULT:
|
|
did_start_interpolating = True
|
|
self.output.write('__result += "" + ')
|
|
if safe is not True:
|
|
self.output.write("__runtime.escape")
|
|
self.output.write("((__tmp = (")
|
|
self.state = STATE_INTERPOLATING
|
|
|
|
def close():
|
|
if did_start_interpolating and self.state == STATE_INTERPOLATING:
|
|
self.output.write(')) == null ? "" : __tmp);')
|
|
self.state = STATE_DEFAULT
|
|
|
|
yield close
|
|
close()
|
|
|
|
@contextlib.contextmanager
|
|
def _scoped_variables(self, nodes_list, **kwargs):
|
|
"""
|
|
Context manager for creating scoped variables defined by the nodes in `nodes_list`.
|
|
These variables will be added to the context, and when the context manager exits the
|
|
context object will be restored to it's previous state.
|
|
"""
|
|
|
|
tmp_vars = []
|
|
for node in nodes_list:
|
|
|
|
is_assign_node = isinstance(node, nodes.Assign)
|
|
name = node.target.name if is_assign_node else node.name
|
|
|
|
# create a temp variable name
|
|
tmp_var = next(self.temp_var_names)
|
|
|
|
# save previous context value
|
|
with self._execution():
|
|
|
|
# save the current value of this name
|
|
self.output.write(
|
|
"var %s = %s.%s;" % (tmp_var, self.context_name, name)
|
|
)
|
|
|
|
# add new value to context
|
|
self.output.write("%s.%s = " % (self.context_name, name))
|
|
|
|
if is_assign_node:
|
|
self._process_node(node.node, **kwargs)
|
|
else:
|
|
self.output.write(node.name)
|
|
|
|
self.output.write(";")
|
|
|
|
tmp_vars.append((tmp_var, name))
|
|
|
|
yield
|
|
|
|
# restore context
|
|
for tmp_var, name in tmp_vars:
|
|
with self._execution():
|
|
self.output.write("%s.%s = %s;" % (self.context_name, name, tmp_var))
|
|
|
|
@contextlib.contextmanager
|
|
def _python_bool_wrapper(self, **kwargs):
|
|
|
|
use_python_bool_wrapper = kwargs.get("use_python_bool_wrapper")
|
|
|
|
if use_python_bool_wrapper:
|
|
self.output.write("__runtime.boolean(")
|
|
|
|
with option(kwargs, use_python_bool_wrapper=False):
|
|
yield kwargs
|
|
|
|
if use_python_bool_wrapper:
|
|
self.output.write(")")
|
|
|
|
|
|
def main(template_path, output=None, data_path=None):
|
|
"""Convert a jinja2 template to a JavaScript module.
|
|
|
|
template_path (Path): Path to .jijna file.
|
|
output (Optional[Path]): Path to output .js module (stdout if unset).
|
|
data_path (Optional[Path]): Optional JSON or YAML file with additional data
|
|
to be included in the JS module as the exported variable DATA.
|
|
"""
|
|
data = "{}"
|
|
if data_path is not None:
|
|
if data_path.suffix in (".yml", ".yaml"):
|
|
data = srsly.read_yaml(data_path)
|
|
else:
|
|
data = srsly.read_json(data_path)
|
|
data = srsly.json_dumps(data) # dump and load for compactness
|
|
template_path = Path(template_path)
|
|
tpl_file = template_path.parts[-1]
|
|
compiler = JinjaToJS(template_path.parent, tpl_file, js_module_format="es6")
|
|
header = f"// This file was auto-generated by {__file__} based on {tpl_file}"
|
|
data_str = f"export const DATA = {data}"
|
|
result = compiler.get_output()
|
|
if output is not None:
|
|
with output.open("w") as f:
|
|
f.write(f"{header}\n{result}\n{data_str}")
|
|
print(f"Updated {output.parts[-1]}")
|
|
else:
|
|
print(result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
args = sys.argv[1:]
|
|
if not len(args):
|
|
raise ValueError("Need at least one argument: path to .jinja template")
|
|
template_path = Path(args[0])
|
|
output = Path(args[1]) if len(args) > 1 else None
|
|
data_path = Path(args[2]) if len(args) > 2 else None
|
|
main(template_path, output, data_path)
|