python-minijinja-2.12.0/0000775000175000017500000000000015106317536014754 5ustar carstencarstenpython-minijinja-2.12.0/tests/0000775000175000017500000000000015052560140016105 5ustar carstencarstenpython-minijinja-2.12.0/tests/test_security.py0000664000175000017500000000070515052560140021367 0ustar carstencarstenfrom minijinja import Environment def test_private_attrs(): class MyClass: def __init__(self): self.public = 42 self._private = 23 env = Environment() rv = env.eval_expr("[x.public, x._private]", x=MyClass()) assert rv == [42, None] def test_dict_is_always_public(): env = Environment() rv = env.eval_expr("[x.public, x._private]", x={"public": 42, "_private": 23}) assert rv == [42, 23] python-minijinja-2.12.0/tests/test_state.py0000664000175000017500000000527315052560140020645 0ustar carstencarstenfrom minijinja import Environment, safe, pass_state def test_func_state(): env = Environment() @pass_state def my_func(state): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.lookup("my_func") is my_func assert state.env is env return 42 rv = env.render_str( "{% block foo %}{{ my_func() }}{% endblock %}", "template-name", my_func=my_func, bar=23, ) assert rv == "42" def test_global_func_state(): env = Environment() @pass_state def my_func(state): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return 42 env.add_global("my_func", my_func) rv = env.render_str( "{% block foo %}{{ my_func() }}{% endblock %}", "template-name", bar=23, ) assert rv == "42" def test_filter_state(): env = Environment() @pass_state def my_filter(state, value): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return value env.add_filter("myfilter", my_filter) rv = env.render_str( "{% block foo %}{{ 42|myfilter }}{% endblock %}", "template-name", bar=23, ) assert rv == "42" def test_test_state(): env = Environment() @pass_state def my_test(state, value): assert state.name == "template-name" assert state.auto_escape is None assert state.current_block == "foo" assert state.lookup("bar") == 23 assert state.lookup("aha") is None assert state.env is env return True env.add_test("mytest", my_test) rv = env.render_str( "{% block foo %}{{ 42 is mytest }}{% endblock %}", "template-name", bar=23, ) assert rv == "true" def test_temps(): env = Environment() first = True @pass_state def inc(state): nonlocal first if first: assert state.get_temp("counter") is None first = False new = state.get_temp("counter", 0) + 1 state.set_temp("counter", new) return new env.add_global("inc", inc) rv = env.render_str("{{ inc() }} {{ inc() }} {{ inc() }}") assert rv == "1 2 3" python-minijinja-2.12.0/tests/test_basic.py0000664000175000017500000003462715052560140020613 0ustar carstencarstenimport binascii import pytest import posixpath import random import types import sys from functools import total_ordering from minijinja import ( Environment, TemplateError, safe, pass_state, eval_expr, render_str, load_from_path, ) class catch_unraisable_exception: def __init__(self) -> None: self.unraisable = None self._old_hook = None def _hook(self, unraisable): self.unraisable = unraisable def __enter__(self): self._old_hook = sys.unraisablehook sys.unraisablehook = self._hook return self def __exit__(self, exc_type, exc_val, exc_tb): assert self._old_hook is not None sys.unraisablehook = self._old_hook self._old_hook = None del self.unraisable def test_expression(): env = Environment() rv = env.eval_expr("1 + b", b=42) assert rv == 43 rv = env.eval_expr("range(n)", n=10) assert rv == list(range(10)) def test_pass_callable(): def magic(): return [1, 2, 3] env = Environment() rv = env.eval_expr("x()", x=magic) assert rv == [1, 2, 3] def test_callable_attrs(): def hmm(): pass hmm.public_attr = 42 env = Environment() rv = env.eval_expr("[hmm.public_attr, hmm.__module__]", hmm=hmm) assert rv == [42, None] def test_generator(): def hmm(): yield 1 yield 2 yield 3 hmm.public_attr = 42 env = Environment() rv = env.eval_expr("values", values=hmm()) assert isinstance(rv, types.GeneratorType) rv = env.eval_expr("values|list", values=hmm()) assert rv == [1, 2, 3] def test_method_calling(): class MyClass(object): def my_method(self): return 23 def __repr__(self): return "This is X" env = Environment() rv = env.eval_expr("[x ~ '', x.my_method()]", x=MyClass()) assert rv == ["This is X", 23] rv = env.eval_expr("x.items()|list", x={"a": "b"}) assert rv == [("a", "b")] def test_types_passthrough(): tup = (1, 2, 3) assert eval_expr("x", x=tup) == tup assert render_str("{{ x }}", x=tup) == "(1, 2, 3)" assert eval_expr("x is sequence", x=tup) == True assert render_str("{{ x }}", x=(1, True)) == "(1, True)" assert eval_expr("x[0] == 42", x=[42]) == True def test_custom_filter(): def my_filter(value): return "<%s>" % value.upper() env = Environment() env.add_filter("myfilter", my_filter) rv = env.eval_expr("'hello'|myfilter") assert rv == "" def test_custom_filter_kwargs(): def my_filter(value, x): return "<%s %s>" % (value.upper(), x) env = Environment() env.add_filter("myfilter", my_filter) rv = env.eval_expr("'hello'|myfilter(x=42)") assert rv == "" def test_custom_test(): def my_test(value, arg): return value == arg env = Environment() env.add_filter("mytest", my_test) rv = env.eval_expr("'hello'|mytest(arg='hello')") assert rv == True rv = env.eval_expr("'hello'|mytest(arg='hellox')") assert rv == False def test_basic_types(): env = Environment() rv = env.eval_expr("{'a': 42, 'b': 42.5, 'c': 'blah'}") assert rv == {"a": 42, "b": 42.5, "c": "blah"} def test_loader(): called = [] def my_loader(name): called.append(name) return "Hello from " + name env = Environment(loader=my_loader) assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("other.html") == "Hello from other.html" assert env.loader is my_loader assert called == ["index.html", "other.html"] env.loader = my_loader assert env.render_template("index.html") == "Hello from index.html" assert called == ["index.html", "other.html"] env.reload() assert env.render_template("index.html") == "Hello from index.html" assert called == ["index.html", "other.html", "index.html"] def test_loader_reload(): called = [] def my_loader(name): called.append(name) return "Hello from " + name env = Environment(loader=my_loader) env.reload_before_render = True assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("index.html") == "Hello from index.html" assert env.render_template("other.html") == "Hello from other.html" assert called == ["index.html", "index.html", "other.html"] def test_autoescape(): assert Environment().auto_escape_callback is None def auto_escape(name): assert name == "foo.html" return "html" env = Environment( auto_escape_callback=auto_escape, loader=lambda x: "Hello {{ foo }}", ) assert env.auto_escape_callback is auto_escape rv = env.render_template("foo.html", foo="") assert rv == "Hello <x>" with catch_unraisable_exception() as cm: rv = env.render_template("invalid.html", foo="") assert rv == "Hello " assert cm.unraisable[0] is AssertionError def test_finalizer(): assert Environment().finalizer is None @pass_state def my_finalizer(state, value): assert state.name == "" if value is None: return "" elif isinstance(value, bytes): return binascii.b2a_hex(value).decode("utf-8") return NotImplemented env = Environment(finalizer=my_finalizer) rv = env.render_str("[{{ foo }}]") assert rv == "[]" rv = env.render_str("[{{ foo }}]", foo=None) assert rv == "[]" rv = env.render_str("[{{ foo }}]", foo="test") assert rv == "[test]" rv = env.render_str("[{{ foo }}]", foo=b"test") assert rv == "[74657374]" def raising_finalizer(value): 1 / 0 env = Environment(finalizer=raising_finalizer) with pytest.raises(ZeroDivisionError): env.render_str("{{ whatever }}") def test_globals(): env = Environment(globals={"x": 23, "y": lambda: 42}) rv = env.eval_expr("[x, y(), z]", z=11) assert rv == [23, 42, 11] def test_honor_safe(): env = Environment(auto_escape_callback=lambda x: True) rv = env.render_str("{{ x }} {{ y }}", x=safe(""), y="") assert rv == " <bar>" def test_full_object_transfer(): class X(object): def __init__(self): self.x = 1 self.y = 2 def test_filter(value): assert isinstance(value, X) return value env = Environment(filters=dict(testfilter=test_filter)) rv = env.eval_expr("x|testfilter", x=X()) assert isinstance(rv, X) assert rv.x == 1 assert rv.y == 2 def test_markup_transfer(): env = Environment() rv = env.eval_expr("value", value=safe("")) assert hasattr(rv, "__html__") assert rv.__html__() == "" rv = env.eval_expr("''|escape") assert hasattr(rv, "__html__") assert rv.__html__() == "<test>" def test_error(): env = Environment() try: env.eval_expr("1 +") except TemplateError as e: assert e.name == "" assert "unexpected end of input" in e.message assert "1 > 1 +" not in e.message assert "1 > 1 +" in str(e) assert e.line == 1 assert e.kind == "SyntaxError" assert e.range == (2, 3) assert e.template_source == "1 +" assert "unexpected end of input" in e.detail else: assert False, "expected error" def test_custom_syntax(): env = Environment( block_start_string="[%", block_end_string="%]", variable_start_string="{", variable_end_string="}", comment_start_string="/*", comment_end_string="*/", ) rv = env.render_str("[% if true %]{value}[% endif %]/* nothing */", value=42) assert rv == "42" def test_path_join(): def join_path(name, parent): return posixpath.join(posixpath.dirname(parent), name) env = Environment( path_join_callback=join_path, templates={ "foo/bar.txt": "{% include 'baz.txt' %}", "foo/baz.txt": "I am baz!", }, ) with catch_unraisable_exception() as cm: rv = env.render_template("foo/bar.txt") assert rv == "I am baz!" assert cm.unraisable is None def test_keep_trailing_newline(): env = Environment(keep_trailing_newline=False) assert env.render_str("foo\n") == "foo" env = Environment(keep_trailing_newline=True) assert env.render_str("foo\n") == "foo\n" def test_trim_blocks(): env = Environment(trim_blocks=False) assert env.render_str("{% if true %}\nfoo{% endif %}") == "\nfoo" env = Environment(trim_blocks=True) assert env.render_str("{% if true %}\nfoo{% endif %}") == "foo" def test_lstrip_blocks(): env = Environment(lstrip_blocks=False) assert env.render_str(" {% if true %}\nfoo{% endif %}") == " \nfoo" env = Environment(lstrip_blocks=True) assert env.render_str(" {% if true %}\nfoo{% endif %}") == "\nfoo" def test_trim_and_lstrip_blocks(): env = Environment(lstrip_blocks=False, trim_blocks=False) assert env.render_str(" {% if true %}\nfoo{% endif %}") == " \nfoo" env = Environment(lstrip_blocks=True, trim_blocks=True) assert env.render_str(" {% if true %}\nfoo{% endif %}") == "foo" def test_line_statements(): env = Environment() assert env.line_statement_prefix is None assert env.line_comment_prefix is None env = Environment(line_statement_prefix="#", line_comment_prefix="##") assert env.line_statement_prefix == "#" assert env.line_comment_prefix == "##" rv = env.render_str("# for x in range(3)\n{{ x }}\n# endfor") assert rv == "0\n1\n2\n" def test_custom_delimiters(): env = Environment( variable_start_string="${", variable_end_string="}", block_start_string="<%", block_end_string="%>", comment_start_string="", ) rv = env.render_str("<% if true %>${ value }<% endif %>", value=42) assert rv == "42" def test_undeclared_variables(): env = Environment( templates={ "foo.txt": "{{ foo }} {{ bar.x }}", "bar.txt": "{{ x }}", } ) assert env.undeclared_variables_in_str("{{ foo }}") == {"foo"} assert env.undeclared_variables_in_str("{{ foo }} {{ bar.x }}") == {"foo", "bar"} assert env.undeclared_variables_in_str("{{ foo }} {{ bar.x }}", nested=True) == { "foo", "bar.x", } assert env.undeclared_variables_in_template("foo.txt") == {"foo", "bar"} assert env.undeclared_variables_in_template("bar.txt") == {"x"} assert env.undeclared_variables_in_template("foo.txt", nested=True) == { "foo", "bar.x", } def test_loop_controls(): env = Environment() rv = env.render_str(""" {% for x in [1, 2, 3, 4, 5] %} {% if x == 1 %} {% continue %} {% elif x == 3 %} {% break %} {% endif %} {{ x }} {% endfor %} """) assert rv.split() == ["2"] def test_pass_through_sort(): @total_ordering class X(object): def __init__(self, value): self.value = value def __eq__(self, other): if type(self) is not type(other): return NotImplemented return self.value == other.value def __lt__(self, other): if type(self) is not type(other): return NotImplemented return self.value < other.value def __str__(self): return str(self.value) values = [X(4), X(23), X(42), X(-1)] env = Environment() rv = env.render_str("{{ values|sort|join(',') }}", values=values) assert rv == "-1,4,23,42" def test_fucked_up_object(): @total_ordering class X: __lt__ = __eq__ = lambda s, o: random.random() > 0.5 values = [X()] * 500 env = Environment() with pytest.raises( TemplateError, match="invalid operation: failed to sort: user-provided comparison function does not correctly implement a total order", ): env.eval_expr("values|sort", values=values) def test_threading_interactions(): from time import time from concurrent.futures import ThreadPoolExecutor done = [] def busy_wait(value, seconds: float): start = time() while time() - start < seconds: continue done.append(value) return value env = Environment(filters={"busy_wait": busy_wait}) executor = ThreadPoolExecutor() for _ in range(4): executor.submit(lambda: env.render_str("{{ 'something' | busy_wait(0.1) }}")) executor.shutdown(wait=True) assert done == ["something"] * 4 def test_truthy(): class Custom: def __init__(self, is_true): self.is_true = is_true def __bool__(self): return bool(self.is_true) env = Environment() assert env.eval_expr("x|bool", x=Custom(True)) is True assert env.eval_expr("x|bool", x=Custom(False)) is False assert env.eval_expr("x|bool", x=Custom(None)) is False assert env.eval_expr("x|bool", x=Custom("")) is False assert env.eval_expr("x|bool", x=Custom("foo")) is True class Fallback: def __bool__(self): raise RuntimeError("swallowed but true") assert env.eval_expr("x|bool", x=Fallback()) is True def test_load_from_path(): env = Environment(loader=load_from_path("tests/templates")) rv = env.render_template("base.txt", woot="woot") assert rv.strip() == "I am from foo! woot!" with pytest.raises(TemplateError) as e: env.render_template("missing.txt") assert e.value.kind == "TemplateNotFound" with pytest.raises(TemplateError) as e: env.render_template("../test_basic.py") assert e.value.kind == "TemplateNotFound" def test_pycompat(): env = Environment() assert env.eval_expr("{'x': 42}.get('x')") == 42 env.pycompat = False with pytest.raises(TemplateError) as e: assert env.eval_expr("{'x': 42}.get('x')") assert "unknown method: map has no method named get" in e.value.message def test_striptags(): env = Environment() assert env.eval_expr("'foo'|striptags") == "foo" assert env.eval_expr("'ä'|striptags") == "ä" def test_attribute_lookups(): class X: def __getattr__(self, _): raise RuntimeError('boom') env = Environment() with pytest.raises(RuntimeError, match="boom"): env.eval_expr("x.foo", x=X()) python-minijinja-2.12.0/tests/templates/0000775000175000017500000000000015052560140020103 5ustar carstencarstenpython-minijinja-2.12.0/tests/templates/base.txt0000664000175000017500000000004115052560140021551 0ustar carstencarsten{% include "includes/foo.txt" %} python-minijinja-2.12.0/tests/templates/includes/0000775000175000017500000000000015052560140021711 5ustar carstencarstenpython-minijinja-2.12.0/tests/templates/includes/foo.txt0000664000175000017500000000003315052560140023231 0ustar carstencarstenI am from foo! {{ woot }}! python-minijinja-2.12.0/uv.lock0000664000175000017500000006430515052560140016257 0ustar carstencarstenversion = 1 revision = 2 requires-python = ">=3.8" resolution-markers = [ "python_full_version >= '3.9'", "python_full_version < '3.9'", ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "maturin" version = "1.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/bc/c7df50a359c3a31490785c77d1ddd5fc83cc8cc07a4eddd289dbae53545a/maturin-1.8.6.tar.gz", hash = "sha256:0e0dc2e0bfaa2e1bd238e0236cf8a2b7e2250ccaa29c1aa8d0e61fa664b0289d", size = 203320, upload-time = "2025-05-13T13:56:11.033Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/f1/e493add40aebdab88ac1aefb41b9c84ac288fb00025bc96b9213ee02c958/maturin-1.8.6-py3-none-linux_armv6l.whl", hash = "sha256:1bf4c743dd2b24448e82b8c96251597818956ddf848e1d16b59356512c7e58d8", size = 7831195, upload-time = "2025-05-13T13:55:42.634Z" }, { url = "https://files.pythonhosted.org/packages/f0/1c/588afdb7bf79c4f15e33e9af6d7f3b12ec662bc63a22919e3bf39afa2a1e/maturin-1.8.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4ea89cf76048bc760e12b36b608fc3f5ef4f7359c0895e9afe737be34041d948", size = 15276471, upload-time = "2025-05-13T13:55:46.196Z" }, { url = "https://files.pythonhosted.org/packages/62/0b/4ce97f7f3a42068fbb42ba47d8b072e098c060fc1f64d8523e5588c57543/maturin-1.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4dd2e2f005ca63ac7ef0dddf2d65324ee480277a11544dcc4e7e436af68034dd", size = 7966129, upload-time = "2025-05-13T13:55:48.633Z" }, { url = "https://files.pythonhosted.org/packages/84/bf/4eae9d12920c580baf9d47ee63fec3ae0233e90a3aa8987bd7909cdc36a0/maturin-1.8.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:b0637604774e2c50ab48a0e9023fe2f071837ecbc817c04ec28e1cfcc25224c2", size = 7835935, upload-time = "2025-05-13T13:55:51.235Z" }, { url = "https://files.pythonhosted.org/packages/f9/aa/8090f8b3f5f7ec46bc95deb0f5b29bf52c98156ef594f2e65d20bf94cea1/maturin-1.8.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:bec5948c6475954c8089b17fae349966258756bb2ca05e54099e476a08070795", size = 8282553, upload-time = "2025-05-13T13:55:53.266Z" }, { url = "https://files.pythonhosted.org/packages/bc/50/4348da6cc16c006dab4e6cd479cf00dc0afa80db289a115a314df9909ee6/maturin-1.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:62a65f70ebaadd6eb6083f5548413744f2ef12400445778e08d41d4facf15bbe", size = 7618339, upload-time = "2025-05-13T13:55:55.706Z" }, { url = "https://files.pythonhosted.org/packages/ce/77/16458e29487d068c8cdb7f06a4403393568a10b44993fe2ec9c3b29fdccd/maturin-1.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:5c0ff7ad43883920032b63c94c76dcdd5758710d7b72b68db69e7826c40534ac", size = 7686748, upload-time = "2025-05-13T13:55:57.97Z" }, { url = "https://files.pythonhosted.org/packages/0c/c1/a52f3c1171c053810606c7c7fae5ce4637446ef9df44f281862d2bef3750/maturin-1.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ca30fdb158a24cf312f3e53072a6e987182c103fa613efea2d28b5f52707d04a", size = 9767394, upload-time = "2025-05-13T13:56:00.694Z" }, { url = "https://files.pythonhosted.org/packages/e2/ab/abae74f36a0f200384eda985ebeb9ee5dcbb19bfe1558c3335ef6f297094/maturin-1.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1e786ec9b5f7315c7e3fcc62b0715f9d99ffe477b06d0d62655a71e6a51a67b", size = 10995574, upload-time = "2025-05-13T13:56:03.101Z" }, { url = "https://files.pythonhosted.org/packages/8e/1c/c478578a62c1e34b5b0641a474de78cb56d6c4aad0ba88f90dfa9f2a15f7/maturin-1.8.6-py3-none-win32.whl", hash = "sha256:dade5edfaf508439ff6bbc7be4f207e04c0999c47d9ef7e1bae16258e76b1518", size = 7027171, upload-time = "2025-05-13T13:56:05.472Z" }, { url = "https://files.pythonhosted.org/packages/c9/89/2c57d29f25e06696cb3c5b3770ba0b40dfe87f91a879ecbcdc92e071a26b/maturin-1.8.6-py3-none-win_amd64.whl", hash = "sha256:6bc9281b90cd37e2a7985f2e5d6e3d35a1d64cf6e4d04ce5ed25603d162995b9", size = 7947998, upload-time = "2025-05-13T13:56:07.428Z" }, { url = "https://files.pythonhosted.org/packages/9d/f5/3ee1c6aa4e277323bef38ea0ec07262a9b88711d1a29cb5bb08ce3807a6f/maturin-1.8.6-py3-none-win_arm64.whl", hash = "sha256:24f66624db69b895b134a8f1592efdf04cd223c9b3b65243ad32080477936d14", size = 6684974, upload-time = "2025-05-13T13:56:09.415Z" }, ] [[package]] name = "minijinja" version = "2.11.0" source = { editable = "." } [package.dev-dependencies] dev = [ { name = "maturin" }, { name = "pyright" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.metadata] [package.metadata.requires-dev] dev = [ { name = "maturin", specifier = ">=1.8.6" }, { name = "pyright", specifier = ">=1.1.401" }, { name = "pytest", specifier = ">=8.3.5" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "pyright" version = "1.1.401" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "typing-extensions", version = "4.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/9a/7ab2b333b921b2d6bfcffe05a0e0a0bbeff884bd6fb5ed50cd68e2898e53/pyright-1.1.401.tar.gz", hash = "sha256:788a82b6611fa5e34a326a921d86d898768cddf59edde8e93e56087d277cc6f1", size = 3894193, upload-time = "2025-05-21T10:44:52.03Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/e6/1f908fce68b0401d41580e0f9acc4c3d1b248adcff00dfaad75cd21a1370/pyright-1.1.401-py3-none-any.whl", hash = "sha256:6fde30492ba5b0d7667c16ecaf6c699fab8d7a1263f6a18549e0b00bf7724c06", size = 5629193, upload-time = "2025-05-21T10:44:50.129Z" }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, { name = "iniconfig", marker = "python_full_version < '3.9'" }, { name = "packaging", marker = "python_full_version < '3.9'" }, { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "tomli", marker = "python_full_version < '3.9'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "pytest" version = "8.4.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, { name = "iniconfig", marker = "python_full_version >= '3.9'" }, { name = "packaging", marker = "python_full_version >= '3.9'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pygments", marker = "python_full_version >= '3.9'" }, { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "typing-extensions" version = "4.14.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.9'", ] sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ] python-minijinja-2.12.0/Cargo.toml0000664000175000017500000000117115052560140016673 0ustar carstencarsten[package] name = "minijinja-py" version = "2.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "minijinja_py" crate-type = ["cdylib"] [dependencies] minijinja = { version = "2.12.0", path = "../minijinja", features = ["loader", "json", "urlencode", "fuel", "preserve_order", "speedups", "custom_syntax", "loop_controls", "internal_safe_search"] } minijinja-contrib = { version = "2.12.0", path = "../minijinja-contrib", features = ["pycompat", "html_entities"] } pyo3 = { version = "0.23.4", features = ["extension-module", "serde", "abi3-py38"] } python-minijinja-2.12.0/LICENSE0000664000175000017500000002513715052560140015760 0ustar carstencarsten Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-minijinja-2.12.0/hello.py0000664000175000017500000000107315052560140016421 0ustar carstencarstenfrom minijinja import Environment INDEX = """{% extends "layout.html" %} {% block title %}{{ page.title }}{% endblock %} {% block body %}
    {%- for item in items %}
  • {{ item }} {%- endfor %}
{% endblock %} """ LAYOUT = """ {% block title %}{% endblock %} {% block body %}{% endblock %} """ env = Environment(templates={ "index.html": INDEX, "layout.html": LAYOUT, }) print(env.render_template( 'index.html', page={"title": "The Page Title"}, items=["Peter", "Paul", "Mary"] )) python-minijinja-2.12.0/python/0000775000175000017500000000000015052560140016264 5ustar carstencarstenpython-minijinja-2.12.0/python/minijinja/0000775000175000017500000000000015052560140020234 5ustar carstencarstenpython-minijinja-2.12.0/python/minijinja/_internal.py0000664000175000017500000000105615052560140022563 0ustar carstencarsten# This file contains functions that the rust module imports. from . import TemplateError, safe def make_error(info): # Internal utility function used by the rust binding to create a template error # with info object. We cannot directly create an error on the Rust side because # we want to subclass the runtime error, but on the limited abi it's not possible # to create subclasses (yet!) err = TemplateError(info.description) err._info = info return err # used by the rust runtime to mark something as safe mark_safe = safe python-minijinja-2.12.0/python/minijinja/__init__.py0000664000175000017500000001547015052560140022354 0ustar carstencarstenimport pathlib from . import _lowlevel __all__ = [ "Environment", "TemplateError", "safe", "escape", "render_str", "eval_expr", "pass_state", ] def handle_panic(orig): def decorator(f): from functools import wraps @wraps(orig) def protected_call(*args, **kwargs): try: return f(*args, **kwargs) except BaseException as e: if e.__class__.__name__ == "PanicException": info = _lowlevel.get_panic_info() message, loc = info or ("unknown panic", None) raise TemplateError( "panic during rendering: {} ({})".format( message, loc or "unknown location" ) ) raise return protected_call return decorator class Environment(_lowlevel.Environment): """Represents a MiniJinja environment""" def __new__(cls, *args, **kwargs): # `_lowlevel.Environment` does not accept any arguments return super().__new__(cls) def __init__( self, loader=None, templates=None, filters=None, tests=None, globals=None, debug=True, fuel=None, undefined_behavior=None, auto_escape_callback=None, path_join_callback=None, keep_trailing_newline=False, trim_blocks=False, lstrip_blocks=False, finalizer=None, reload_before_render=False, block_start_string="{%", block_end_string="%}", variable_start_string="{{", variable_end_string="}}", comment_start_string="{#", comment_end_string="#}", line_statement_prefix=None, line_comment_prefix=None, pycompat=True, ): super().__init__() if loader is not None: if templates: raise TypeError("Cannot set loader and templates at the same time") self.loader = loader elif templates is not None: self.loader = dict(templates).get if fuel is not None: self.fuel = fuel if filters: for name, callback in filters.items(): self.add_filter(name, callback) if tests: for name, callback in tests.items(): self.add_test(name, callback) if globals is not None: for name, value in globals.items(): self.add_global(name, value) self.debug = debug if auto_escape_callback is not None: self.auto_escape_callback = auto_escape_callback if path_join_callback is not None: self.path_join_callback = path_join_callback if keep_trailing_newline: self.keep_trailing_newline = True if trim_blocks: self.trim_blocks = True if lstrip_blocks: self.lstrip_blocks = True if finalizer is not None: self.finalizer = finalizer if undefined_behavior is not None: self.undefined_behavior = undefined_behavior self.reload_before_render = reload_before_render # XXX: because this is not an atomic reconfigure if you set one of # the values to a conflicting set, it will immediately error out :( self.block_start_string = block_start_string self.block_end_string = block_end_string self.variable_start_string = variable_start_string self.variable_end_string = variable_end_string self.comment_start_string = comment_start_string self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.line_comment_prefix = line_comment_prefix self.pycompat = pycompat @handle_panic(_lowlevel.Environment.render_str) def render_str(self, *args, **kwargs): return super().render_str(*args, **kwargs) @handle_panic(_lowlevel.Environment.eval_expr) def eval_expr(self, *args, **kwargs): return super().eval_expr(*args, **kwargs) DEFAULT_ENVIRONMENT = Environment() def render_str(*args, **context): """Shortcut to render a string with the default environment.""" return DEFAULT_ENVIRONMENT.render_str(*args, **context) def eval_expr(*args, **context): """Evaluate an expression with the default environment.""" return DEFAULT_ENVIRONMENT.eval_expr(*args, **context) try: from markupsafe import escape, Markup except ImportError: from html import escape as _escape class Markup(str): def __html__(self): return self def escape(value): callback = getattr(value, "__html__", None) if callback is not None: return callback() return Markup(_escape(str(value))) def safe(s): """Marks a string as safe.""" return Markup(s) def pass_state(f): """Pass the engine state to the function as first argument.""" f.__minijinja_pass_state__ = True return f def load_from_path(paths): """Load a template from one or more paths.""" if isinstance(paths, (str, pathlib.Path)): paths = [paths] def loader(name): if "\\" in name: return None pieces = name.strip("/").split("/") if ".." in pieces: return None for path in paths: p = pathlib.Path(path).joinpath(*pieces) if p.is_file(): return p.read_text() return loader class TemplateError(RuntimeError): """Represents a runtime error in the template engine.""" def __init__(self, message): super().__init__(message) self._info = None @property def message(self): """The short message of the error.""" return self.args[0] @property def kind(self): """The kind of the error.""" if self._info is None: return "Unknown" else: return self._info.kind @property def name(self): """The name of the template.""" if self._info is not None: return self._info.name @property def detail(self): """The detail error message of the error.""" if self._info is not None: return self._info.detail @property def line(self): """The line of the error.""" if self._info is not None: return self._info.line @property def range(self): """The range of the error.""" if self._info is not None: return self._info.range @property def template_source(self): """The template source of the error.""" if self._info is not None: return self._info.template_source def __str__(self): if self._info is not None: return self._info.full_description return self.message del handle_panic python-minijinja-2.12.0/python/minijinja/__init__.pyi0000664000175000017500000001357515052560140022531 0ustar carstencarstenfrom pathlib import PurePath import pathlib from typing import ( Any, Callable, Iterable, Literal, TypeVar, Protocol, overload, ) from typing_extensions import Final, TypeAlias, Self from minijinja._lowlevel import State from collections.abc import Mapping __all__ = [ "Environment", "TemplateError", "safe", "escape", "render_str", "eval_expr", "pass_state", ] _A_contra = TypeVar("_A_contra", contravariant=True) _R_co = TypeVar("_R_co", covariant=True) class _PassesState(Protocol[_A_contra, _R_co]): def __call__(self, state: State, value: _A_contra, /) -> _R_co: ... __minijinja_pass_state__: Literal[True] _StrPath: TypeAlias = PurePath | str _Behavior = Literal["strict", "lenient", "chainable"] DEFAULT_ENVIRONMENT: Final[Environment] def render_str(source: str, name: str | None = None, /, **context: Any) -> str: ... def eval_expr(expression: str, /, **context: Any) -> Any: ... class Environment: loader: Callable[[str], str | None] | None fuel: int | None debug: bool pycompat: bool undefined_behavior: _Behavior auto_escape_callback: Callable[[str], str | bool | None] | None path_join_callback: Callable[[str, str], _StrPath] | None keep_trailing_newline: bool trim_blocks: bool lstrip_blocks: bool finalizer: _PassesState[Any, Any] | Callable[[Any], Any] | None reload_before_render: bool block_start_string: str block_end_string: str variable_start_string: str variable_end_string: str comment_start_string: str comment_end_string: str line_statement_prefix: str | None line_comment_prefix: str | None globals: dict[str, Any] @overload def __init__( self, loader: Callable[[str], str | None] | None = None, templates: Mapping[str, str] | None = None, filters: Mapping[str, Callable[[Any], Any]] | None = None, tests: Mapping[str, Callable[[Any], bool]] | None = None, globals: Mapping[str, Any] | None = None, debug: bool = True, fuel: int | None = None, undefined_behavior: _Behavior = "lenient", auto_escape_callback: Callable[[str], str | bool | None] | None = None, path_join_callback: Callable[[str, str], _StrPath] | None = None, keep_trailing_newline: bool = False, trim_blocks: bool = False, lstrip_blocks: bool = False, finalizer: _PassesState[Any, Any] | None = None, reload_before_render: bool = False, block_start_string: str = "{%", block_end_string: str = "%}", variable_start_string: str = "{{", variable_end_string: str = "}}", comment_start_string: str = "{#", comment_end_string: str = "#}", line_statement_prefix: str | None = None, line_comment_prefix: str | None = None, pycompat: bool = True, ) -> None: ... @overload def __init__( self, loader: Callable[[str], str | None] | None = None, templates: Mapping[str, str] | None = None, filters: Mapping[str, Callable[..., Any]] | None = None, tests: Mapping[str, Callable[..., bool]] | None = None, globals: Mapping[str, Any] | None = None, debug: bool = True, fuel: int | None = None, undefined_behavior: _Behavior = "lenient", auto_escape_callback: Callable[[str], str | bool | None] | None = None, path_join_callback: Callable[[str, str], _StrPath] | None = None, keep_trailing_newline: bool = False, trim_blocks: bool = False, lstrip_blocks: bool = False, finalizer: Callable[[Any], Any] | None = None, reload_before_render: bool = False, block_start_string: str = "{%", block_end_string: str = "%}", variable_start_string: str = "{{", variable_end_string: str = "}}", comment_start_string: str = "{#", comment_end_string: str = "#}", line_statement_prefix: str | None = None, line_comment_prefix: str | None = None, pycompat: bool = True, ) -> None: ... def add_template(self, name: str, source: str) -> None: ... def remove_template(self, name: str) -> None: ... def add_filter(self, name: str, filter: Callable[..., Any]) -> None: ... def remove_filter(self, name: str) -> None: ... def add_test(self, name: str, test: Callable[..., bool]) -> None: ... def remove_test(self, name: str) -> None: ... def add_global(self, name: str, value: Any) -> None: ... def remove_global(self, name: str) -> None: ... def clear_templates(self) -> None: ... def reload(self) -> None: ... def render_template(self, template_name: str, /, **context: Any) -> str: ... def render_str( self, source: str, name: str | None = None, /, **context: Any ) -> str: ... def undeclared_variables_in_str( self, source: str, nested: bool = False ) -> set[str]: ... def undeclared_variables_in_template( self, template_name: str, nested: bool = False ) -> set[str]: ... def eval_expr(self, expression: str, /, **context: Any) -> Any: ... class TemplateError(RuntimeError): def __init__(self, message: str) -> None: ... @property def message(self) -> str: ... @property def kind(self) -> str: ... @property def name(self) -> str | None: ... @property def detail(self) -> str | None: ... @property def line(self) -> int | None: ... @property def range(self) -> tuple[int, int] | None: ... @property def template_source(self) -> str | None: ... def __str__(self) -> str: ... class Markup(str): def __html__(self) -> Self: ... def safe(value: str) -> str: ... def escape(value: Any) -> str: ... def pass_state( f: Callable[[State, _A_contra], _R_co], ) -> _PassesState[_A_contra, _R_co]: ... Path = str | pathlib.Path def load_from_path(paths: Iterable[Path] | Path) -> Callable[[str], str | None]: ... python-minijinja-2.12.0/python/minijinja/_lowlevel.pyi0000664000175000017500000000102015052560140022740 0ustar carstencarstenfrom typing import Any, Optional from minijinja import Environment class State: """A reference to the current state.""" @property def env(self) -> Environment: ... @property def name(self) -> str: ... @property def auto_escape(self) -> Optional[str]: ... @property def current_block(self) -> Optional[str]: ... def lookup(self, name: str) -> Any: ... def get_temp(self, name: str, default: Optional[Any] = None) -> Any: ... def set_temp(self, name: str, value: Any) -> Any: ... python-minijinja-2.12.0/README.md0000664000175000017500000002034415052560140016225 0ustar carstencarsten

MiniJinja for Python: a powerful template engine for Rust and Python

[![License](https://img.shields.io/github/license/mitsuhiko/minijinja)](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) [![Crates.io](https://img.shields.io/crates/d/minijinja.svg)](https://crates.io/crates/minijinja) [![rustc 1.63.0](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg) [![Documentation](https://docs.rs/minijinja/badge.svg)](https://docs.rs/minijinja)
`minijinja-py` is an experimental binding of [MiniJinja](https://github.com/mitsuhiko/minijinja) to Python. It has somewhat limited functionality compared to the Rust version. These bindings use [maturin](https://www.maturin.rs/) and [pyo3](https://pyo3.rs/). You might want to use MiniJinja instead of Jinja2 when the full feature set of Jinja2 is not required and you want to have the same rendering experience of a data set between Rust and Python. With these bindings MiniJinja can render some Python objects and values that are passed to templates, but there are clear limitations with regards to what can be done. To install MiniJinja for Python you can fetch the package [from PyPI](https://pypi.org/project/minijinja/): ``` $ pip install minijinja ``` ## Basic API The basic API is hidden behind the `Environment` object. It behaves almost entirely like in `minijinja` with some Python specific changes. For instance instead of `env.set_debug(True)` you use `env.debug = True`. Additionally instead of using `add_template` or attaching a `source` you either pass a dictionary of templates directly to the environment or a `loader` function. ```python from minijinja import Environment env = Environment(templates={ "template_name": "Template source" }) ``` To render a template you can use the `render_template` method: ```python result = env.render_template('template_name', var1="value 1", var2="value 2") print(result) ``` ## Purpose MiniJinja attempts a reasonably high level of compatibility with Jinja2, but it does not try to achieve this at all costs. As a result you will notice that quite a few templates will refuse to render with MiniJinja despite the fact that they probably look quite innocent. It is however possible to write templates that render to the same results for both Jinja2 and MiniJinja. This raises the question why you might want to use MiniJinja. The main benefit would be to achieve the exact same results in both Rust and Python. Additionally MiniJinja has a stronger sandbox than Jinja2 and might perform ever so slightly better in some situations. However you should be aware that due to the marshalling that needs to happen in either direction there is a certain amount of loss of information. ## Dynamic Template Loading MiniJinja's Python bindings inherit the underlying behavior of how MiniJinja loads templates. Templates are loaded on first use and then cached. The templates are loaded via a loader. To trigger a reload you can call `env.reload()` or alternatively set `env.reload_before_render` to `True`. ```python def my_loader(name): segments = [] for segment in name.split("/"): if "\\" in segment or segment in (".", ".."): return None segments.append(segment) try: with open(os.path.join(TEMPLATES, *segments)) as f: return f.read() except (IOError, OSError): pass env = Environment(loader=my_loader) env.reload_before_render = True print(env.render_template("index.html")) ``` Alternatively templates can manually be loaded and unloaded with `env.add_template` and `env.remove_template`. ## Auto Escaping The default behavior is to use auto escaping file files ending in `.html`. You can customize this behavior by overriding the `auto_escape_callback`: ```python env = Environment(auto_escape_callback=lambda x: x.endswith((".html", ".foo"))) ``` MiniJinja uses [markupsafe](https://github.com/pallets/markupsafe) if it's available on the Python side. It will honor `__html__`. ## Finalizers Instead of custom formatters like in MiniJinja, you can define a finalizer instead which is similar to how it works in Jinja2. It's passed a value (or optional also the state as first argument when `pass_state` is used) and can return a new value. If the special `NotImplemented` value is returned, the original value is rendered without any modification: ``` from minijinja import Environment def finalizer(value): if value is None: return "" return NotImplemented env = Environment(finalizer=finalizer) assert env.render_str("{{ none }}") == "" ``` ## State Access Functions passed to the environment such as filters or global functions can optionally have the template state passed by using the `pass_state` parameter. This is similar to `pass_context` in Jinja2. It can be used to look at the name of the template or to look up variables in the context. ```python from minijinja import pass_state @pass_state def my_filter(state, value): return state.lookup("a_variable") + value env.add_filter("add_a_variable", my_filter) ``` ## Runtime Behavior MiniJinja uses it's own runtime model which is not matching the Python runtime model. As a result there are gaps in behavior between the two but some limited effort is made to bridge them. For instance you will be able to call some methods of types, but for instance builtins such as dicts and lists do not expose their methods on the MiniJinja side in all cases. A natively generated MiniJinja map (such as with the `dict` global function) will not have an `.items()` method, whereas a Python dict passed to MiniJinja will. Here is what this means for some basic types: * Python dictionaries and lists (as well as other objects that behave as sequences) appear in the MiniJinja side very similar to how they do in Python. * Tuples on the MiniJinja side are represented as lists, but will appear again as tuples if passed back to Python. * Python objects are represented in MiniJinja similarly to dicts, but they retain all their meaningful Python APIs. This means they stringify via `__str__` and they allow the MiniJinja code to call their non-underscored methods. Note that there is no extra security layer in use at the moment so take care of what you pass there. * MiniJinja's python binding understand what `__html__` is when it exists on a string subclass. This means that a `markupsafe.Markup` object will appear as safe string in MiniJinja. This information can also flow back to Python again. * Stringification of objects uses `__str__` which is why mixed Python and MiniJinja objects can be a bit confusing at times. * Where in Jinja2 there is a difference between `foo["bar"]` and `foo.bar` which can be used to disambiugate properties and keys, in MiniJinja there is no such difference. However methods are disambiugated so `foo.items()` works and will correctly call the method in all cases. * Operator overloading is not supported. This in particular means that operators like `+` will not invoke the `__add__` method of most objects. This is an intentional limitation of the engine. * When MiniJinja objects are exposed to the Python side they lose most of their functionality. For instance plain objects such as functions are currently just represented as strings, maps are dictionaries etc. This also means you cannot call methods on them from the Python side. ## Threading MiniJinja's Python bindin is thread-safe but it uses locks internally on the environment. In particular only one thread can render a template from the same environment at the time. If you want to render templates from multiple threads you should be creating a new environment for each thread. ## Sponsor If you like the project and find it useful you can [become a sponsor](https://github.com/sponsors/mitsuhiko). ## License and Links - [Documentation](https://docs.rs/minijinja/) - [Examples](https://github.com/mitsuhiko/minijinja/tree/main/examples) - [Issue Tracker](https://github.com/mitsuhiko/minijinja/issues) - [MiniJinja Playground](https://mitsuhiko.github.io/minijinja-playground/) - License: [Apache-2.0](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) python-minijinja-2.12.0/src/0000775000175000017500000000000015052560140015532 5ustar carstencarstenpython-minijinja-2.12.0/src/environment.rs0000664000175000017500000007005515052560140020453 0ustar carstencarstenuse std::borrow::Cow; use std::collections::HashSet; use std::ffi::c_void; use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use std::sync::{Arc, Mutex}; use minijinja::syntax::SyntaxConfig; use minijinja::value::{Rest, Value}; use minijinja::{ context, escape_formatter, AutoEscape, Error, ErrorKind, State, UndefinedBehavior, }; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyDict, PyTuple}; use crate::error_support::{report_unraisable, to_minijinja_error, to_py_error}; use crate::state::bind_state; use crate::typeconv::{ get_custom_autoescape, to_minijinja_value, to_python_args, to_python_value, DynamicObject, }; thread_local! { static CURRENT_ENV: AtomicPtr = const { AtomicPtr::new(std::ptr::null_mut()) }; } struct Syntax { block_start: String, block_end: String, variable_start: String, variable_end: String, comment_start: String, comment_end: String, line_statement_prefix: String, line_comment_prefix: String, } impl Default for Syntax { fn default() -> Self { Self { block_start: "{%".into(), block_end: "%}".into(), variable_start: "{{".into(), variable_end: "}}".into(), comment_start: "{#".into(), comment_end: "#}".into(), line_statement_prefix: "".into(), line_comment_prefix: "".into(), } } } impl Syntax { fn compile(&self) -> Result { SyntaxConfig::builder() .block_delimiters(self.block_start.clone(), self.block_end.clone()) .variable_delimiters(self.variable_start.clone(), self.variable_end.clone()) .comment_delimiters(self.comment_start.clone(), self.comment_end.clone()) .line_statement_prefix(self.line_statement_prefix.clone()) .line_comment_prefix(self.line_comment_prefix.clone()) .build() } } macro_rules! syntax_setter { ($slf:expr, $value:expr, $field:ident, $default:expr) => {{ let value = $value; let mut inner = $slf.inner.lock().unwrap(); if inner.syntax.is_none() { if value == $default { return Ok(()); } inner.syntax = Some(Syntax::default()); } if let Some(ref mut syntax) = inner.syntax { if syntax.$field != value { syntax.$field = value.into(); let syntax_config = syntax.compile().map_err(to_py_error)?; inner.env.set_syntax(syntax_config); } } Ok(()) }}; } macro_rules! syntax_getter { ($slf:expr, $field:ident, $default:expr) => {{ $slf.inner .lock() .unwrap() .syntax .as_ref() .map_or($default, |x| &x.$field) .into() }}; } struct Inner { env: minijinja::Environment<'static>, loader: Option>, auto_escape_callback: Option>, finalizer_callback: Option>, path_join_callback: Option>, syntax: Option, } /// Represents a MiniJinja environment. #[pyclass(subclass, module = "minijinja._lowlevel")] pub struct Environment { inner: Arc>, reload_before_render: AtomicBool, pycompat: Arc, } #[pymethods] impl Environment { #[new] fn py_new() -> PyResult { let mut env = minijinja::Environment::new(); minijinja_contrib::add_to_environment(&mut env); let pycompat = Arc::new(AtomicBool::new(false)); let pycompat_weak = Arc::downgrade(&pycompat); env.set_unknown_method_callback(move |state, value, method, args| { if let Some(pycompat) = pycompat_weak.upgrade() { if pycompat.load(Ordering::Relaxed) { return minijinja_contrib::pycompat::unknown_method_callback( state, value, method, args, ); } } Err(Error::from(ErrorKind::UnknownMethod)) }); Ok(Environment { inner: Arc::new(Mutex::new(Inner { env, loader: None, auto_escape_callback: None, finalizer_callback: None, path_join_callback: None, syntax: None, })), reload_before_render: AtomicBool::new(false), pycompat, }) } /// Enables or disables debug mode. #[setter] pub fn set_debug(&self, value: bool) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_debug(value); Ok(()) } /// Enables or disables debug mode. #[getter] pub fn get_debug(&self) -> PyResult { let inner = self.inner.lock().unwrap(); Ok(inner.env.debug()) } /// Enables or disables pycompat mode. #[setter] pub fn set_pycompat(&self, value: bool) -> PyResult<()> { self.pycompat.store(value, Ordering::Relaxed); Ok(()) } /// Enables or disables pycompat mode. #[getter] pub fn get_pycompat(&self) -> PyResult { Ok(self.pycompat.load(Ordering::Relaxed)) } /// Sets the undefined behavior. #[setter] pub fn set_undefined_behavior(&self, value: &str) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_undefined_behavior(match value { "strict" => UndefinedBehavior::Strict, "lenient" => UndefinedBehavior::Lenient, "chainable" => UndefinedBehavior::Chainable, _ => { return Err(PyRuntimeError::new_err( "invalid value for undefined behavior", )) } }); Ok(()) } /// Gets the undefined behavior. #[getter] pub fn get_undefined_behavior(&self) -> PyResult<&'static str> { let inner = self.inner.lock().unwrap(); Ok(match inner.env.undefined_behavior() { UndefinedBehavior::Lenient => "lenient", UndefinedBehavior::Chainable => "chainable", UndefinedBehavior::Strict => "strict", _ => { return Err(PyRuntimeError::new_err( "invalid value for undefined behavior", )) } }) } /// Sets fuel #[setter] pub fn set_fuel(&self, value: Option) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner.env.set_fuel(value); Ok(()) } /// Enables or disables debug mode. #[getter] pub fn get_fuel(&self) -> PyResult> { let inner = self.inner.lock().unwrap(); Ok(inner.env.fuel()) } /// Registers a filter function. #[pyo3(text_signature = "(self, name, callback)")] pub fn add_filter(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_filter( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py))) }) }) }, ); Ok(()) } /// Removes a filter function. #[pyo3(text_signature = "(self, name)")] pub fn remove_filter(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_filter(name); Ok(()) } /// Registers a test function. #[pyo3(text_signature = "(self, name, callback)")] pub fn add_test(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_test( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py)).is_true()) }) }) }, ); Ok(()) } /// Removes a test function. #[pyo3(text_signature = "(self, name)")] pub fn remove_test(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_test(name); Ok(()) } fn add_function(&self, name: &str, callback: &Bound<'_, PyAny>) -> PyResult<()> { let callback: Py = callback.clone().unbind(); self.inner.lock().unwrap().env.add_function( name.to_string(), move |state: &State, args: Rest| -> Result { Python::with_gil(|py| { bind_state(state, || { let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), &args) .map_err(to_minijinja_error)?; let rv = callback .call(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; Ok(to_minijinja_value(rv.bind(py))) }) }) }, ); Ok(()) } /// Registers a global #[pyo3(text_signature = "(self, name, value)")] pub fn add_global(&self, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { if value.is_callable() { self.add_function(name, value) } else { self.inner .lock() .unwrap() .env .add_global(name.to_string(), to_minijinja_value(value)); Ok(()) } } /// Removes a global #[pyo3(text_signature = "(self, name)")] pub fn remove_global(&self, name: &str) -> PyResult<()> { self.inner.lock().unwrap().env.remove_global(name); Ok(()) } /// The set of known global variables. #[getter] pub fn globals(&self, py: Python<'_>) -> PyResult { let rv = PyDict::new(py); for (key, value) in self.inner.lock().unwrap().env.globals() { rv.set_item(key, to_python_value(value)?)?; } Ok(rv.into()) } /// Sets an auto escape callback. /// /// Note that because this interface in MiniJinja is infallible, the callback is /// not able to raise an error. #[setter] pub fn set_auto_escape_callback( &self, py: Python<'_>, callback: &Bound<'_, PyAny>, ) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); inner.auto_escape_callback = Some(callback.clone_ref(py)); inner .env .set_auto_escape_callback(move |name: &str| -> AutoEscape { Python::with_gil(|py| { let py_args = PyTuple::new(py, [name]).unwrap(); let rv = match callback.call(py, py_args, None) { Ok(value) => value, Err(err) => { report_unraisable(py, err); return AutoEscape::None; } }; let rv = rv.bind(py); if rv.is_none() { return AutoEscape::None; } if let Ok(value) = rv.extract::() { match &value as &str { "html" => AutoEscape::Html, "json" => AutoEscape::Json, other => get_custom_autoescape(other), } } else if let Ok(value) = rv.extract::() { match value { true => AutoEscape::Html, false => AutoEscape::None, } } else { AutoEscape::None } }) }); Ok(()) } #[getter] pub fn get_auto_escape_callback(&self, py: Python<'_>) -> PyResult>> { Ok(self .inner .lock() .unwrap() .auto_escape_callback .as_ref() .map(|x| x.clone_ref(py))) } /// Sets a finalizer. /// /// A finalizer is called before a value is rendered to customize it. #[setter] pub fn set_finalizer(&self, py: Python<'_>, callback: &Bound<'_, PyAny>) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); inner.finalizer_callback = Some(callback.clone_ref(py)); inner.env.set_formatter(move |output, state, value| { Python::with_gil(|py| -> Result<(), Error> { let maybe_new_value = bind_state(state, || -> Result<_, Error> { let args = std::slice::from_ref(value); let (py_args, py_kwargs) = to_python_args(py, callback.bind(py), args).map_err(to_minijinja_error)?; let rv = callback .call(py, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?; if rv.is(&py.NotImplemented()) { Ok(None) } else { Ok(Some(to_minijinja_value(rv.bind(py)))) } })?; let value = match maybe_new_value { Some(ref new_value) => new_value, None => value, }; escape_formatter(output, state, value) }) }); Ok(()) } #[getter] pub fn get_finalizer(&self, py: Python<'_>) -> PyResult>> { Ok(self .inner .lock() .unwrap() .finalizer_callback .as_ref() .map(|x| x.clone_ref(py))) } /// Sets a loader function for the environment. /// /// The loader function is invoked with the name of the template to load. If the /// template exists the source code of the template should be returned a string, /// otherwise `None` can be used to indicate that the template does not exist. #[setter] pub fn set_loader(&self, py: Python<'_>, callback: Option<&Bound<'_, PyAny>>) -> PyResult<()> { let callback = match callback { None => None, Some(callback) => { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } Some(callback.clone().unbind()) } }; let mut inner = self.inner.lock().unwrap(); inner.loader = callback.as_ref().map(|x| x.clone_ref(py)); if let Some(callback) = callback { inner.env.set_loader(move |name| { Python::with_gil(|py| { let callback = callback.bind(py); let rv = callback .call1(PyTuple::new(py, [name]).unwrap()) .map_err(to_minijinja_error)?; if rv.is_none() { Ok(None) } else { Ok(Some(rv.to_string())) } }) }) } Ok(()) } /// Returns the current loader. #[getter] pub fn get_loader(&self, py: Python<'_>) -> Option> { self.inner .lock() .unwrap() .loader .as_ref() .map(|x| x.clone_ref(py)) } /// Sets a new path join callback. #[setter] pub fn set_path_join_callback( &self, py: Python<'_>, callback: &Bound<'_, PyAny>, ) -> PyResult<()> { if !callback.is_callable() { return Err(PyRuntimeError::new_err("expected callback")); } let callback: Py = callback.clone().unbind(); let mut inner = self.inner.lock().unwrap(); inner.path_join_callback = Some(callback.clone_ref(py)); inner.env.set_path_join_callback(move |name, parent| { Python::with_gil(|py| { let callback = callback.bind(py); match callback.call1(PyTuple::new(py, [name, parent]).unwrap()) { Ok(rv) => Cow::Owned(rv.to_string()), Err(err) => { report_unraisable(py, err); Cow::Borrowed(name) } } }) }); Ok(()) } /// Returns the current path join callback. #[getter] pub fn get_path_join_callback(&self, py: Python<'_>) -> Option> { self.inner .lock() .unwrap() .path_join_callback .as_ref() .map(|x| x.clone_ref(py)) } /// Triggers a reload of the templates. pub fn reload(&self, py: Python<'_>) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); let loader = inner.loader.as_ref().map(|x| x.clone_ref(py)); if loader.is_some() { inner.env.clear_templates(); } Ok(()) } /// Can be used to instruct the environment to automatically reload templates /// before each render. #[setter] pub fn set_reload_before_render(&self, yes: bool) { self.reload_before_render.store(yes, Ordering::Relaxed); } #[getter] pub fn get_reload_before_render(&self) -> bool { self.reload_before_render.load(Ordering::Relaxed) } #[setter] pub fn set_variable_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, variable_start, "{{") } #[getter] pub fn get_variable_start_string(&self) -> String { syntax_getter!(self, variable_start, "{{") } #[setter] pub fn set_block_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, block_start, "{%") } #[getter] pub fn get_block_start_string(&self) -> String { syntax_getter!(self, block_start, "{%") } #[setter] pub fn set_comment_start_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, comment_start, "{#") } #[getter] pub fn get_comment_start_string(&self) -> String { syntax_getter!(self, comment_start, "{#") } #[setter] pub fn set_variable_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, variable_end, "}}") } #[getter] pub fn get_variable_end_string(&self) -> String { syntax_getter!(self, variable_end, "}}") } #[setter] pub fn set_block_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, block_end, "%}") } #[getter] pub fn get_block_end_string(&self) -> String { syntax_getter!(self, block_end, "%}") } #[setter] pub fn set_comment_end_string(&self, value: String) -> PyResult<()> { syntax_setter!(self, value, comment_end, "#}") } #[getter] pub fn get_comment_end_string(&self) -> String { syntax_getter!(self, comment_end, "#}") } #[setter] pub fn set_line_statement_prefix(&self, value: Option) -> PyResult<()> { syntax_setter!(self, value.unwrap_or_default(), line_statement_prefix, "") } #[getter] pub fn get_line_statement_prefix(&self) -> Option { let rv: String = syntax_getter!(self, line_statement_prefix, ""); (!rv.is_empty()).then_some(rv) } #[setter] pub fn set_line_comment_prefix(&self, value: Option) -> PyResult<()> { syntax_setter!(self, value.unwrap_or_default(), line_comment_prefix, "") } #[getter] pub fn get_line_comment_prefix(&self) -> Option { let rv: String = syntax_getter!(self, line_comment_prefix, ""); (!rv.is_empty()).then_some(rv) } /// Configures the trailing newline trimming feature. #[setter] pub fn set_keep_trailing_newline(&self, yes: bool) -> PyResult<()> { self.inner .lock() .unwrap() .env .set_keep_trailing_newline(yes); Ok(()) } /// Returns the current value of the trailing newline trimming flag. #[getter] pub fn get_keep_trailing_newline(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.keep_trailing_newline()) } /// Configures the trim blocks feature. #[setter] pub fn set_trim_blocks(&self, yes: bool) -> PyResult<()> { self.inner.lock().unwrap().env.set_trim_blocks(yes); Ok(()) } /// Returns the current value of the trim blocks flag. #[getter] pub fn get_trim_blocks(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.trim_blocks()) } /// Configures the lstrip blocks feature. #[setter] pub fn set_lstrip_blocks(&self, yes: bool) -> PyResult<()> { self.inner.lock().unwrap().env.set_lstrip_blocks(yes); Ok(()) } /// Returns the current value of the lstrip blocks flag. #[getter] pub fn get_lstrip_blocks(&self) -> PyResult { Ok(self.inner.lock().unwrap().env.lstrip_blocks()) } /// Manually adds a template to the environment. pub fn add_template(&self, name: String, source: String) -> PyResult<()> { let mut inner = self.inner.lock().unwrap(); inner .env .add_template_owned(name, source) .map_err(to_py_error) } /// Removes a loaded template. pub fn remove_template(&self, name: &str) { self.inner.lock().unwrap().env.remove_template(name); } /// Clears all loaded templates. pub fn clear_templates(&self) { self.inner.lock().unwrap().env.clear_templates(); } /// Renders a template looked up from the loader. /// /// The first argument is the name of the template, all other arguments must be passed /// as keyword arguments and are pass as render context of the template. #[pyo3(signature = (template_name, /, **ctx))] pub fn render_template( slf: PyRef<'_, Self>, py: Python<'_>, template_name: &str, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult { if slf.reload_before_render.load(Ordering::Relaxed) { slf.reload(py)?; } let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); bind_environment(slf.as_ptr(), || { let inner = slf.inner.clone(); py.allow_threads(move || { let inner = inner.lock().unwrap(); let tmpl = inner.env.get_template(template_name).map_err(to_py_error)?; tmpl.render(ctx).map_err(to_py_error) }) }) } /// Finds undeclared variables in a template. #[pyo3(signature = (template_name, nested = false))] pub fn undeclared_variables_in_template( slf: PyRef<'_, Self>, py: Python<'_>, template_name: &str, nested: bool, ) -> PyResult> { if slf.reload_before_render.load(Ordering::Relaxed) { slf.reload(py)?; } bind_environment(slf.as_ptr(), || { let inner = slf.inner.lock().unwrap(); let tmpl = inner.env.get_template(template_name).map_err(to_py_error)?; Ok(tmpl.undeclared_variables(nested)) }) } /// Finds undeclared variables in a template string. #[pyo3(signature = (source, nested = false))] pub fn undeclared_variables_in_str( slf: PyRef<'_, Self>, py: Python<'_>, source: &str, nested: bool, ) -> PyResult> { if slf.reload_before_render.load(Ordering::Relaxed) { slf.reload(py)?; } bind_environment(slf.as_ptr(), || { let inner = slf.inner.lock().unwrap(); let tmpl = inner.env.template_from_str(source).map_err(to_py_error)?; Ok(tmpl.undeclared_variables(nested)) }) } /// Renders a template from a string /// /// The first argument is the source of the template, all other arguments must be passed /// as keyword arguments and are pass as render context of the template. #[pyo3(signature = (source, name=None, /, **ctx))] pub fn render_str( slf: PyRef<'_, Self>, py: Python<'_>, source: &str, name: Option<&str>, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult { bind_environment(slf.as_ptr(), || { let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); let inner = slf.inner.clone(); py.allow_threads(move || { inner .lock() .unwrap() .env .render_named_str(name.unwrap_or(""), source, ctx) .map_err(to_py_error) }) }) } /// Evaluates an expression with a given context. #[pyo3(signature = (expression, /, **ctx))] pub fn eval_expr( slf: PyRef<'_, Self>, py: Python<'_>, expression: &str, ctx: Option<&Bound<'_, PyDict>>, ) -> PyResult> { bind_environment(slf.as_ptr(), || { let inner = slf.inner.clone(); let ctx = ctx .map(|ctx| Value::from_object(DynamicObject::new(ctx.as_any().clone().unbind()))) .unwrap_or_else(|| context!()); py.allow_threads(move || { let inner = inner.lock().unwrap(); let expr = inner .env .compile_expression(expression) .map_err(to_py_error)?; to_python_value(expr.eval(ctx).map_err(to_py_error)?) }) }) } } pub fn with_environment) -> PyResult>( py: Python<'_>, f: F, ) -> PyResult { CURRENT_ENV.with(|handle| { let ptr = handle.load(Ordering::Relaxed) as *mut _; match unsafe { Py::::from_borrowed_ptr_or_opt(py, ptr) } { Some(env) => f(env), None => Err(PyRuntimeError::new_err( "environment cannot be used outside of template render", )), } }) } /// Invokes a function with the state stashed away. pub fn bind_environment R>(envptr: *mut pyo3::ffi::PyObject, f: F) -> R { let old_handle = CURRENT_ENV .with(|handle| handle.swap(envptr as *const _ as *mut c_void, Ordering::Relaxed)); let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); CURRENT_ENV.with(|handle| handle.store(old_handle, Ordering::Relaxed)); match rv { Ok(rv) => rv, Err(payload) => std::panic::resume_unwind(payload), } } python-minijinja-2.12.0/src/error_support.rs0000664000175000017500000000711315052560140021027 0ustar carstencarstenuse std::any::Any; use std::cell::RefCell; use minijinja::{Error, ErrorKind}; use pyo3::ffi::PyErr_WriteUnraisable; use pyo3::prelude::*; use pyo3::sync::GILOnceCell; use pyo3::types::PyTuple; static TEMPLATE_ERROR: GILOnceCell> = GILOnceCell::new(); thread_local! { static STASHED_ERROR: RefCell> = const { RefCell::new(None) }; static PANIC_INFO: RefCell)>> = const { RefCell::new(None) }; } /// Provides information about a template error from the runtime. #[pyclass(subclass, module = "minijinja._lowlevel", name = "ErrorInfo")] pub struct ErrorInfo { err: minijinja::Error, } #[pymethods] impl ErrorInfo { #[getter] pub fn get_kind(&self) -> String { format!("{:?}", self.err.kind()) } #[getter] pub fn get_name(&self) -> Option { self.err.name().map(|x| x.into()) } #[getter] pub fn get_line(&self) -> Option { self.err.line() } #[getter] pub fn get_range(&self) -> Option<(usize, usize)> { self.err.range().map(|x| (x.start, x.end)) } #[getter] pub fn get_template_source(&self) -> Option<&str> { self.err.template_source() } #[getter] pub fn get_description(&self) -> String { format!("{}", self.err) } #[getter] pub fn get_detail(&self) -> Option<&str> { self.err.detail() } #[getter] pub fn get_full_description(&self) -> String { use std::fmt::Write; let mut rv = format!("{:#}", self.err); let mut err = &self.err as &dyn std::error::Error; while let Some(next_err) = err.source() { rv.push('\n'); writeln!(&mut rv, "caused by: {next_err:#}").unwrap(); err = next_err; } rv } } pub fn to_minijinja_error(err: PyErr) -> Error { let msg = err.to_string(); STASHED_ERROR.with(|stash| { *stash.borrow_mut() = Some(err); }); Error::new(ErrorKind::TemplateNotFound, msg) } pub fn to_py_error(original_err: Error) -> PyErr { STASHED_ERROR.with(|stash| { stash .borrow_mut() .take() .unwrap_or_else(|| make_error(original_err)) }) } pub fn report_unraisable(py: Python<'_>, err: PyErr) { err.restore(py); unsafe { PyErr_WriteUnraisable(std::ptr::null_mut()); } } fn make_error(err: Error) -> PyErr { Python::with_gil(|py| { let template_error: &Py = TEMPLATE_ERROR.get_or_init(py, || { let module = py.import("minijinja._internal").unwrap(); let err = module.getattr("make_error").unwrap(); err.into() }); let args = PyTuple::new(py, [Bound::new(py, ErrorInfo { err }).unwrap()]).unwrap(); PyErr::from_value(template_error.call1(py, args).unwrap().bind(py).clone()) }) } fn payload_as_str(payload: &dyn Any) -> &str { if let Some(&s) = payload.downcast_ref::<&'static str>() { s } else if let Some(s) = payload.downcast_ref::() { s.as_str() } else { "unknown error" } } pub(crate) fn init_panic_hook() { std::panic::set_hook(Box::new(|info| { let msg = payload_as_str(info.payload()); let loc = info.location(); PANIC_INFO.with(|stash| { let str_loc = loc.map(|loc| format!("{}:{}", loc.file(), loc.line())); *stash.borrow_mut() = Some((msg.to_string(), str_loc)); }); })); } #[pyfunction] pub(crate) fn get_panic_info() -> PyResult)>> { Ok(PANIC_INFO.with(|stash| stash.borrow().clone())) } python-minijinja-2.12.0/src/state.rs0000664000175000017500000000757215052560140017233 0ustar carstencarstenuse minijinja::{AutoEscape, State}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use std::ffi::c_void; use std::sync::atomic::{AtomicPtr, Ordering}; use crate::environment::{with_environment, Environment}; use crate::typeconv::{to_minijinja_value, to_python_value}; thread_local! { static CURRENT_STATE: AtomicPtr = const { AtomicPtr::new(std::ptr::null_mut()) }; } /// A reference to the current state. #[pyclass(subclass, module = "minijinja._lowlevel", name = "State")] pub struct StateRef; #[pymethods] impl StateRef { /// Returns a reference to the environment. #[getter] pub fn get_env(&self, py: Python<'_>) -> PyResult> { with_environment(py, Ok) } /// Returns the name of the template. #[getter] pub fn get_name(&self) -> PyResult { with_state(|state| Ok(state.name().to_string())) } /// Returns the current auto escape flag #[getter] pub fn get_auto_escape(&self) -> PyResult> { with_state(|state| { Ok(match state.auto_escape() { AutoEscape::None => None, AutoEscape::Html => Some("html"), AutoEscape::Json => Some("json"), AutoEscape::Custom(custom) => Some(custom), _ => None, }) }) } /// Returns the current block #[getter] pub fn get_current_block(&self) -> PyResult> { with_state(|state| Ok(state.current_block().map(|x| x.into()))) } /// Looks up a variable in the context #[pyo3(text_signature = "(self, name)")] pub fn lookup(&self, name: &str) -> PyResult> { with_state(|state| { state .lookup(name) .map(to_python_value) .unwrap_or_else(|| Ok(Python::with_gil(|py| py.None()))) }) } /// Looks up a temp by name. #[pyo3(signature = (name, default = None))] pub fn get_temp(&self, name: &str, default: Option<&Bound<'_, PyAny>>) -> PyResult> { with_state(|state| { let rv = state.get_temp(name); match rv { Some(rv) => to_python_value(rv), None => { if let Some(default) = default { let val = to_minijinja_value(default); state.set_temp(name, val.clone()); to_python_value(val) } else { Ok(Python::with_gil(|py| py.None())) } } } }) } /// Sets a temp by name and returns the old value. #[pyo3(text_signature = "(self, name, value)")] pub fn set_temp(&self, name: &str, value: &Bound<'_, PyAny>) -> PyResult> { with_state(|state| { state .set_temp(name, to_minijinja_value(value)) .map(to_python_value) .unwrap_or_else(|| Ok(Python::with_gil(|py| py.None()))) }) } } pub fn with_state PyResult>(f: F) -> PyResult { CURRENT_STATE.with(|handle| { match unsafe { (handle.load(Ordering::Relaxed) as *const State).as_ref() } { Some(state) => f(state), None => Err(PyRuntimeError::new_err( "state cannot be used outside of template render", )), } }) } /// Invokes a function with the state stashed away. pub fn bind_state R>(state: &State, f: F) -> R { let old_handle = CURRENT_STATE .with(|handle| handle.swap(state as *const _ as *mut c_void, Ordering::Relaxed)); let rv = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); CURRENT_STATE.with(|handle| handle.store(old_handle, Ordering::Relaxed)); match rv { Ok(rv) => rv, Err(payload) => std::panic::resume_unwind(payload), } } python-minijinja-2.12.0/src/typeconv.rs0000664000175000017500000002630615052560140017756 0ustar carstencarstenuse std::cmp::Ordering; use std::collections::BTreeMap; use std::fmt; use std::sync::{Arc, Mutex}; use minijinja::value::{DynObject, Enumerator, Object, ObjectRepr, Value, ValueKind}; use minijinja::{AutoEscape, Error, State}; use pyo3::exceptions::{PyAttributeError, PyLookupError, PyTypeError}; use pyo3::pybacked::PyBackedStr; use pyo3::sync::GILOnceCell; use pyo3::types::{PyDict, PyList, PySequence, PyTuple}; use pyo3::{prelude::*, IntoPyObjectExt}; use crate::error_support::{to_minijinja_error, to_py_error}; use crate::state::{bind_state, StateRef}; static AUTO_ESCAPE_CACHE: Mutex> = Mutex::new(BTreeMap::new()); static MARK_SAFE: GILOnceCell> = GILOnceCell::new(); fn is_safe_attr(name: &str) -> bool { !name.starts_with('_') } fn is_dictish(val: &Bound<'_, PyAny>) -> bool { val.hasattr("__getitem__").unwrap_or(false) && val.hasattr("items").unwrap_or(false) } pub struct DynamicObject { pub inner: Py, } impl DynamicObject { pub fn new(inner: Py) -> DynamicObject { DynamicObject { inner } } } impl fmt::Debug for DynamicObject { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Python::with_gil(|py| write!(f, "{}", self.inner.bind(py))) } } impl Object for DynamicObject { fn repr(self: &Arc) -> ObjectRepr { Python::with_gil(|py| { let inner = self.inner.bind(py); if inner.downcast::().is_ok() { ObjectRepr::Seq } else if is_dictish(inner) { ObjectRepr::Map } else if inner.try_iter().is_ok() { ObjectRepr::Iterable } else { ObjectRepr::Plain } }) } fn render(self: &Arc, f: &mut fmt::Formatter<'_>) -> fmt::Result where Self: Sized + 'static, { Python::with_gil(|py| write!(f, "{}", self.inner.bind(py))) } fn call(self: &Arc, state: &State, args: &[Value]) -> Result { Python::with_gil(|py| -> Result { bind_state(state, || { let inner = self.inner.bind(py); let (py_args, py_kwargs) = to_python_args(py, inner, args).map_err(to_minijinja_error)?; Ok(to_minijinja_value( &inner .call(py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?, )) }) }) } fn call_method( self: &Arc, state: &State, name: &str, args: &[Value], ) -> Result { if !is_safe_attr(name) { return Err(Error::new( minijinja::ErrorKind::InvalidOperation, "insecure method call", )); } Python::with_gil(|py| -> Result { bind_state(state, || { let inner = self.inner.bind(py); let (py_args, py_kwargs) = to_python_args(py, inner, args).map_err(to_minijinja_error)?; Ok(to_minijinja_value( &inner .call_method(name, py_args, py_kwargs.as_ref()) .map_err(to_minijinja_error)?, )) }) }) } fn get_value(self: &Arc, key: &Value) -> Option { Python::with_gil(|py| { let inner = self.inner.bind(py); match inner.get_item(to_python_value_impl(py, key.clone()).ok()?) { Ok(value) => Some(to_minijinja_value(&value)), Err(err) => { if err.is_instance_of::(py) || err.is_instance_of::(py) || err.is_instance_of::(py) { if let Some(attr) = key.as_str() { if is_safe_attr(attr) { match inner.getattr(attr) { Ok(rv) => return Some(to_minijinja_value(&rv)), Err(attr_err) => { if !attr_err.is_instance_of::(py) { return Some(Value::from(to_minijinja_error(attr_err))); } } } } } None } else { Some(Value::from(to_minijinja_error(err))) } } } }) } fn custom_cmp(self: &Arc, other: &DynObject) -> Option { // Attention: this can violate the requirements of custom_cmp, // namely that it implements a total order. Python::with_gil(|py| { let self_inner = self.inner.bind(py); let other = other.downcast_ref::()?; let other_inner = other.inner.bind(py); self_inner.compare(other_inner).ok() }) } fn is_true(self: &Arc) -> bool { Python::with_gil(|py| { let inner = self.inner.bind(py); inner.is_truthy().unwrap_or(true) }) } fn enumerate(self: &Arc) -> Enumerator { Python::with_gil(|py| { let inner = self.inner.bind(py); if inner.downcast::().is_ok() { Enumerator::Seq(inner.len().unwrap_or(0)) } else if let Ok(iter) = inner.try_iter() { Enumerator::Values( iter.filter_map(|x| match x { Ok(x) => Some(to_minijinja_value(&x)), Err(_) => None, }) .collect(), ) } else { Enumerator::NonEnumerable } }) } } pub fn to_minijinja_value(value: &Bound<'_, PyAny>) -> Value { if value.is_none() { Value::from(()) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { Value::from(val) } else if let Ok(val) = value.extract::() { if let Ok(to_html) = value.getattr("__html__") { if to_html.is_callable() { // TODO: if to_minijinja_value returns results we could // report the swallowed error of __html__. if let Ok(html) = to_html.call0() { if let Ok(val) = html.extract::() { return Value::from_safe_string(val.to_string()); } } } } Value::from(val.to_string()) } else { Value::from_object(DynamicObject::new(value.clone().unbind())) } } pub fn to_python_value(value: Value) -> PyResult> { Python::with_gil(|py| to_python_value_impl(py, value)) } fn mark_string_safe(py: Python<'_>, value: &str) -> PyResult> { let mark_safe: &Py = MARK_SAFE.get_or_try_init::<_, PyErr>(py, || { let module = py.import("minijinja._internal")?; Ok(module.getattr("mark_safe")?.into()) })?; mark_safe.call1(py, PyTuple::new(py, [value])?) } fn to_python_value_impl(py: Python<'_>, value: Value) -> PyResult> { // if we are holding a true dynamic object, we want to allow bidirectional // conversion. That means that when passing the object back to Python we // extract the retained raw Python reference. if let Some(pyobj) = value.downcast_object_ref::() { return Ok(pyobj.inner.clone_ref(py)); } if let Some(obj) = value.as_object() { match obj.repr() { ObjectRepr::Plain => return obj.to_string().into_py_any(py), ObjectRepr::Map => { let rv = PyDict::new(py); if let Some(pair_iter) = obj.try_iter_pairs() { for (key, value) in pair_iter { rv.set_item( to_python_value_impl(py, key)?, to_python_value_impl(py, value)?, )?; } } return Ok(rv.into()); } ObjectRepr::Seq | ObjectRepr::Iterable => { let rv = PyList::empty(py); if let Some(iter) = obj.try_iter() { for value in iter { rv.append(to_python_value_impl(py, value)?)?; } } return Ok(rv.into()); } _ => {} } } match value.kind() { ValueKind::Undefined | ValueKind::None => Ok(py.None()), ValueKind::Bool => Ok(value.is_true().into_py_any(py)?), ValueKind::Number => { if let Ok(rv) = TryInto::::try_into(value.clone()) { Ok(rv.into_py_any(py)?) } else if let Ok(rv) = TryInto::::try_into(value.clone()) { Ok(rv.into_py_any(py)?) } else if let Ok(rv) = TryInto::::try_into(value) { Ok(rv.into_py_any(py)?) } else { unreachable!() } } ValueKind::String => { if value.is_safe() { Ok(mark_string_safe(py, value.as_str().unwrap())?) } else { Ok(value.as_str().unwrap().into_py_any(py)?) } } ValueKind::Bytes => Ok(value.as_bytes().unwrap().into_py_any(py)?), kind => Err(to_py_error(minijinja::Error::new( minijinja::ErrorKind::InvalidOperation, format!("object {kind} cannot roundtrip"), ))), } } pub fn to_python_args<'py>( py: Python<'py>, callback: &Bound<'_, PyAny>, args: &[Value], ) -> PyResult<(Bound<'py, PyTuple>, Option>)> { let mut py_args = Vec::new(); let mut py_kwargs = None; if callback .getattr("__minijinja_pass_state__") .is_ok_and(|x| x.is_truthy().unwrap_or(false)) { py_args.push(Bound::new(py, StateRef)?.into_py_any(py)?); } for arg in args { if arg.is_kwargs() { let kwargs = py_kwargs.get_or_insert_with(|| PyDict::new(py)); if let Ok(iter) = arg.try_iter() { for k in iter { if let Ok(v) = arg.get_item(&k) { kwargs .set_item(to_python_value_impl(py, k)?, to_python_value_impl(py, v)?)?; } } } } else { py_args.push(to_python_value_impl(py, arg.clone())?); } } let py_args = PyTuple::new(py, py_args).unwrap(); Ok((py_args, py_kwargs)) } pub fn get_custom_autoescape(value: &str) -> AutoEscape { let mut cache = AUTO_ESCAPE_CACHE.lock().unwrap(); if let Some(rv) = cache.get(value).copied() { return rv; } let val = AutoEscape::Custom(Box::leak(value.to_string().into_boxed_str())); cache.insert(value.to_string(), val); val } python-minijinja-2.12.0/src/lib.rs0000664000175000017500000000066115052560140016651 0ustar carstencarstenuse pyo3::prelude::*; mod environment; mod error_support; mod state; mod typeconv; #[pymodule] fn _lowlevel(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(error_support::get_panic_info, m)?)?; crate::error_support::init_panic_hook(); Ok(()) } python-minijinja-2.12.0/pyproject.toml0000664000175000017500000000252215052560140017660 0ustar carstencarsten[build-system] requires = ["maturin>=1.5"] build-backend = "maturin" [project] name = "minijinja" version = "2.12.0" description = "An experimental Python binding of the Rust MiniJinja template engine." requires-python = ">=3.8" license = { file = "LICENSE" } authors = [ { name = "Armin Ronacher", email = "armin.ronacher@active-4.com" } ] maintainers = [ { name = "Armin Ronacher", email = "armin.ronacher@active-4.com" } ] keywords = ["jinja", "template-engine"] classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Environment :: Web Environment", "License :: OSI Approved :: Apache Software License", "Topic :: Text Processing :: Markup :: HTML", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ] [project.urls] Repository = "https://github.com/mitsuhiko/minijinja" "Issue Tracker" = "https://github.com/mitsuhiko/minijinja/issues" "Donate" = "https://github.com/sponsors/mitsuhiko" [tool.maturin] module-name = "minijinja._lowlevel" python-source = "python" strip = true [tool.pyright] include = ["python/**/*.pyi"] exclude = ["python/**/*.py"] typeCheckingMode = "strict" pythonVersion = "3.9" [dependency-groups] dev = [ "maturin>=1.8.6", "pyright>=1.1.401", "pytest>=8.3.5", ] python-minijinja-2.12.0/minijinja-contrib/0000775000175000017500000000000015106317536020362 5ustar carstencarstenpython-minijinja-2.12.0/minijinja-contrib/tests/0000775000175000017500000000000015052560140021513 5ustar carstencarstenpython-minijinja-2.12.0/minijinja-contrib/tests/datetime.rs0000664000175000017500000001611015052560140023654 0ustar carstencarsten#![cfg(all(feature = "datetime", feature = "timezone"))] use minijinja::context; use similar_asserts::assert_eq; use time::format_description::well_known::Iso8601; #[test] fn test_datetimeformat() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATETIME_FORMAT", "[hour]:[minute]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("1687624642.5|datetimeformat(format=format)") .unwrap(); assert_eq!( expr.eval(context!(format => "short")).unwrap().to_string(), "2023-06-24 18:37" ); assert_eq!( expr.eval(context!(format => "medium")).unwrap().to_string(), "Jun 24 2023 18:37" ); assert_eq!( expr.eval(context!(format => "long")).unwrap().to_string(), "June 24 2023 18:37:22" ); assert_eq!( expr.eval(context!(format => "full")).unwrap().to_string(), "Saturday, June 24 2023 18:37:22.5" ); assert_eq!( expr.eval(context!(format => "unix")).unwrap().to_string(), "1687624642" ); assert_eq!( expr.eval(context!(format => "iso")).unwrap().to_string(), "2023-06-24T18:37:22+02:00" ); let expr = env .compile_expression("1687624642|datetimeformat(tz='Europe/Moscow')") .unwrap(); assert_eq!(expr.eval(()).unwrap().to_string(), "19:37"); } #[test] fn test_datetimeformat_iso_negative() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "America/Chicago"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("1687624642.5|datetimeformat(format='iso')") .unwrap(); assert_eq!( expr.eval(()).unwrap().to_string(), "2023-06-24T11:37:22-05:00" ) } #[test] fn test_datetimeformat_time_rs() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATETIME_FORMAT", "[hour]:[minute]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("d|datetimeformat(format=format)") .unwrap(); let d = time::OffsetDateTime::from_unix_timestamp(1687624642).unwrap(); assert_eq!( expr.eval(context!(d, format => "short")) .unwrap() .to_string(), "2023-06-24 18:37" ); } #[test] fn test_datetimeformat_chrono() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATETIME_FORMAT", "[hour]:[minute]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("d|datetimeformat(format=format)") .unwrap(); let d = chrono::DateTime::parse_from_rfc3339("2023-06-24T16:37:00Z").unwrap(); assert_eq!( expr.eval(context!(d, format => "short")) .unwrap() .to_string(), "2023-06-24 18:37" ); } #[test] fn test_dateformat() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATE_FORMAT", "[year]-[month]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("1687624642.5|dateformat(format=format)") .unwrap(); assert_eq!( expr.eval(context!(format => "short")).unwrap().to_string(), "2023-06-24" ); assert_eq!( expr.eval(context!(format => "medium")).unwrap().to_string(), "Jun 24 2023" ); assert_eq!( expr.eval(context!(format => "long")).unwrap().to_string(), "June 24 2023" ); assert_eq!( expr.eval(context!(format => "full")).unwrap().to_string(), "Saturday, June 24 2023" ); let expr = env .compile_expression("1687624642|dateformat(tz='Europe/Moscow')") .unwrap(); assert_eq!(expr.eval(()).unwrap().to_string(), "2023-06"); } #[test] fn test_dateformat_time_rs() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATE_FORMAT", "[year]-[month]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("d|dateformat(format=format)") .unwrap(); let d = time::Date::from_ordinal_date(2023, 42).unwrap(); assert_eq!( expr.eval(context!(d, format => "short")) .unwrap() .to_string(), "2023-02-11" ); } #[test] fn test_dateformat_chrono_rs() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("DATE_FORMAT", "[year]-[month]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("d|dateformat(format=format)") .unwrap(); let d = chrono::NaiveDate::from_num_days_from_ce_opt(739073); assert_eq!( expr.eval(context!(d, format => "short")) .unwrap() .to_string(), "2024-07-06" ); assert_eq!( expr.eval(context!(d => "2024-07-06", format => "short")) .unwrap() .to_string(), "2024-07-06" ); } #[test] fn test_datetime_format_naive() { let mut env = minijinja::Environment::new(); minijinja_contrib::add_to_environment(&mut env); let d = time::Date::from_calendar_date(2024, time::Month::January, 18) .unwrap() .with_hms(0, 1, 2) .unwrap(); let expr = env .compile_expression("d|datetimeformat(format=format, tz='Europe/Brussels')") .unwrap(); assert_eq!( expr.eval( context!(d => d.format(&Iso8601::DATE_TIME).unwrap().to_string(), format => "iso") ) .unwrap() .to_string(), "2024-01-18T00:01:02+01:00" ); assert_eq!( expr.eval(context!(d, format => "iso")).unwrap().to_string(), "2024-01-18T00:01:02+01:00" ); } #[test] fn test_timeformat() { let mut env = minijinja::Environment::new(); env.add_global("TIMEZONE", "Europe/Vienna"); env.add_global("TIME_FORMAT", "[hour]:[minute]"); minijinja_contrib::add_to_environment(&mut env); let expr = env .compile_expression("1687624642.5|timeformat(format=format)") .unwrap(); assert_eq!( expr.eval(context!(format => "short")).unwrap().to_string(), "18:37" ); assert_eq!( expr.eval(context!(format => "medium")).unwrap().to_string(), "18:37" ); assert_eq!( expr.eval(context!(format => "long")).unwrap().to_string(), "18:37:22" ); assert_eq!( expr.eval(context!(format => "full")).unwrap().to_string(), "18:37:22.5" ); assert_eq!( expr.eval(context!(format => "unix")).unwrap().to_string(), "1687624642" ); assert_eq!( expr.eval(context!(format => "iso")).unwrap().to_string(), "2023-06-24T18:37:22+02:00" ); let expr = env .compile_expression("1687624642|timeformat(tz='Europe/Moscow')") .unwrap(); assert_eq!(expr.eval(()).unwrap().to_string(), "19:37"); } python-minijinja-2.12.0/minijinja-contrib/tests/pycompat.rs0000664000175000017500000000743615052560140023727 0ustar carstencarsten#![cfg(feature = "pycompat")] use minijinja::{Environment, Value}; use minijinja_contrib::pycompat::unknown_method_callback; use similar_asserts::assert_eq; fn eval_expr(expr: &str) -> Value { let mut env = Environment::new(); env.set_unknown_method_callback(unknown_method_callback); env.compile_expression(expr).unwrap().eval(()).unwrap() } fn eval_err_expr(expr: &str) -> String { let mut env = Environment::new(); env.set_unknown_method_callback(unknown_method_callback); env.compile_expression(expr) .unwrap() .eval(()) .unwrap_err() .to_string() } #[test] fn test_string_methods() { assert_eq!(eval_expr("'foo'.upper()").as_str(), Some("FOO")); assert_eq!(eval_expr("'FoO'.lower()").as_str(), Some("foo")); assert_eq!(eval_expr("' foo '.strip()").as_str(), Some("foo")); assert_eq!(eval_expr("'!foo?!!!'.strip('!?')").as_str(), Some("foo")); assert_eq!( eval_expr("'!!!foo?!!!'.rstrip('!?')").as_str(), Some("!!!foo") ); assert_eq!( eval_expr("'!!!foo?!!!'.lstrip('!?')").as_str(), Some("foo?!!!") ); assert!(eval_expr("'foobar'.islower()").is_true()); assert!(eval_expr("'FOOBAR'.isupper()").is_true()); assert!(eval_expr("' \\n'.isspace()").is_true()); assert!(eval_expr("'abc'.isalpha()").is_true()); assert!(eval_expr("'abc123'.isalnum()").is_true()); assert!(eval_expr("'abc%@#'.isascii()").is_true()); assert_eq!( eval_expr("'foobar'.replace('o', 'x')").as_str(), Some("fxxbar") ); assert_eq!( eval_expr("'foobar'.replace('o', 'x', 1)").as_str(), Some("fxobar") ); assert_eq!(eval_expr("'foo bar'.title()").as_str(), Some("Foo Bar")); assert_eq!( eval_expr("'foo bar'.capitalize()").as_str(), Some("Foo bar") ); assert_eq!(eval_expr("'foo barooo'.count('oo')").as_usize(), Some(2)); assert_eq!(eval_expr("'foo barooo'.find('oo')").as_usize(), Some(1)); assert_eq!(eval_expr("'foo barooo'.rfind('oo')").as_usize(), Some(8)); assert!(eval_expr("'a b c'.split() == ['a', 'b', 'c']").is_true()); assert!(eval_expr("'a b c'.split() == ['a', 'b', 'c']").is_true()); assert!(eval_expr("'a b c'.split(none, 1) == ['a', 'b c']").is_true()); assert!(eval_expr("'abcbd'.split('b', 1) == ['a', 'cbd']").is_true()); assert!(eval_expr("'a\\nb\\r\\nc'.splitlines() == ['a', 'b', 'c']").is_true()); assert!(eval_expr("'a\\nb\\r\\nc'.splitlines(true) == ['a\\n', 'b\\r\\n', 'c']").is_true()); assert!(eval_expr("'foobarbaz'.startswith('foo')").is_true()); assert!(eval_expr("'foobarbaz'.startswith(('foo', 'bar'))").is_true()); assert!(!eval_expr("'barfoobaz'.startswith(('foo', 'baz'))").is_true()); assert!(eval_expr("'foobarbaz'.endswith('baz')").is_true()); assert!(eval_expr("'foobarbaz'.endswith(('baz', 'bar'))").is_true()); assert!(!eval_expr("'foobarbazblah'.endswith(('baz', 'bar'))").is_true()); assert_eq!(eval_expr("'|'.join([1, 2, 3])").as_str(), Some("1|2|3")); } #[test] fn test_dict_methods() { assert!(eval_expr("{'x': 42}.keys()|list == ['x']").is_true()); assert!(eval_expr("{'x': 42}.values()|list == [42]").is_true()); assert!(eval_expr("{'x': 42}.items()|list == [('x', 42)]").is_true()); assert!(eval_expr("{'x': 42}.get('x') == 42").is_true()); assert!(eval_expr("{'x': 42}.get('y') is none").is_true()); } #[test] fn test_list_methods() { assert!(eval_expr("[1, 2, 2, 3].count(2) == 2").is_true()); } #[test] fn test_errors() { assert!(eval_err_expr("'abc'.split(1, 2)").contains("value is not a string")); assert!(eval_err_expr("'abc'.startswith(1)") .contains("startswith argument must be string or a tuple of strings, not number")); assert!(eval_err_expr("{'x': 42}.get()").contains("missing argument")); } python-minijinja-2.12.0/minijinja-contrib/tests/globals.rs0000664000175000017500000001212415052560140023504 0ustar carstencarstenuse insta::assert_snapshot; use minijinja::{render, Environment}; use minijinja_contrib::globals::{cycler, joiner}; #[test] fn test_cycler() { let mut env = Environment::new(); env.add_function("cycler", cycler); assert_snapshot!(render!(in env, r"{% set c = cycler([1, 2]) -%} next(): {{ c.next() }} next(): {{ c.next() }} next(): {{ c.next() }} cycler: {{ c }}"), @r###" next(): 1 next(): 2 next(): 1 cycler: Cycler { items: [1, 2], pos: 1 } "###); } #[test] fn test_joiner() { let mut env = Environment::new(); env.add_function("joiner", joiner); assert_snapshot!(render!(in env, r"{% set j = joiner() -%} first: [{{ j() }}] second: [{{ j() }}] joiner: {{ j }}"), @r###" first: [] second: [, ] joiner: Joiner { sep: ", ", used: true } "###); assert_snapshot!(render!(in env, r"{% set j = joiner('|') -%} first: [{{ j() }}] second: [{{ j() }}] joiner: {{ j }}"), @r###" first: [] second: [|] joiner: Joiner { sep: "|", used: true } "###); } #[test] #[cfg(feature = "rand")] #[cfg(target_pointer_width = "64")] fn test_lispum() { // The small rng is pointer size specific. Test on 64bit platforms only use minijinja_contrib::globals::lipsum; let mut env = Environment::new(); env.add_function("lipsum", lipsum); assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ lipsum(5) }}"), @r###" Nulla curae morbi nec gravida scelerisque habitant facilisi eros lectus molestie mattis neque dignissim. Convallis per ssociis erat ipsum pellentesque. Imperdiet enim egestas feugiat adipiscing sociosqu vulputate malesuada fames elit massa arcu eleifend porta morbi lectus. Aenean metus risus elit pede nec morbi hendrerit ssociis natoque gravida montes ssociis ante gravida dignissim. Congue sapien augue sociosqu ssociis aptent id ridiculus eu sed imperdiet enim aliquam hendrerit rutrum. Quisque bibendum neque ssociis porta mauris ssociis sociis facilisi gravida proin metus imperdiet luctus. Nam natoque pulvinar dolor sociosqu aptent at sociosqu placerat malesuada placerat et. Fusce aptent hymenaeos mauris leo elit morbi proin cum consectetuer cras ssociis lacus maecenas. Ad potenti duis ssociis ante hymenaeos mi dictum ligula dictum. Ridiculus ssociis ac habitasse ssociis maecenas lacinia diam faucibus porta diam magna. Mus laoreet mollis sociosqu ssociis mus mollis praesent ssociis molestie habitant inceptos. Sociosqu class congue eu luctus rhoncus dolor sem natoque mattis hymenaeos fusce nunc. Egestas habitant cum pulvinar parturient ssociis sociosqu metus mus aliquam libero nec platea curabitur orci nisi. Purus dapibus nunc arcu donec cursus ornare dui in porttitor potenti a nascetur. S nisi posuere pretium hac lacinia pulvinar senectus platea dis mattis semper condimentum convallis cursus dis. Mattis ssociis inceptos duis. Platea donec vsociis ssociis aliquet non ssociis sit placerat nostra lacus habitant ssociis. Phasellus hymenaeos arcu sit magnis dis at nisi cras curabitur sociosqu eget sociis cras. Aenean duis iaculis platea donec sociosqu lacus pretium ssociis pellentesque risus ssociis nam nonummy. Adipiscing rutrum in commodo non hac vsociis etiam vulputate sapien sociosqu potenti aliquam. Quis cras nostra senectus amet adipiscing duis aliquam semper etiam elementum. Ssociis mi in laoreet at sociis ssociis. Neque eros ssociis faucibus euismod est interdum nam quis condimentum dis natoque a ssociis nec. Duis erat mollis cubilia faucibus rhoncus pellentesque laoreet commodo mi imperdiet pede ssociis. Parturient lobortis ssociis quam lectus nec ac dui maecenas orci netus fringilla magnis curabitur justo. Sed porta phasellus molestie cubilia nunc luctus platea mattis platea nullam elementum cursus ornare. Ssociis scelerisque. "###); assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ lipsum(2, html=true) }}"), @r###"

Nulla curae morbi nec gravida scelerisque habitant facilisi eros lectus molestie mattis neque dignissim. Convallis per ssociis erat ipsum pellentesque.

Imperdiet enim egestas feugiat adipiscing sociosqu vulputate malesuada fames elit massa arcu eleifend porta morbi lectus. Aenean metus risus elit pede nec morbi hendrerit ssociis natoque gravida montes ssociis ante gravida dignissim. Congue sapien augue sociosqu ssociis aptent id ridiculus eu sed imperdiet enim aliquam hendrerit rutrum. Quisque bibendum neque ssociis porta mauris ssociis sociis facilisi gravida proin metus imperdiet luctus. Nam natoque pulvinar dolor sociosqu aptent at sociosqu placerat malesuada placerat et. Fusce aptent hymenaeos mauris leo elit morbi proin cum consectetuer cras ssociis lacus maecenas. Ad potenti duis ssociis ante hymenaeos mi dictum ligula dictum.

"###); } #[test] #[cfg(feature = "rand")] fn test_randrange() { use minijinja_contrib::globals::randrange; let mut env = Environment::new(); env.add_function("randrange", randrange); assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ randrange(10) }}|{{ randrange(10) }}"), @"0|6"); assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ randrange(-50, 50) }}"), @"-50"); } python-minijinja-2.12.0/minijinja-contrib/tests/filters.rs0000664000175000017500000003112115052560140023527 0ustar carstencarstenuse minijinja::{context, Environment}; use minijinja_contrib::filters::{pluralize, striptags}; use similar_asserts::assert_eq; #[test] fn test_pluralize() { let mut env = Environment::new(); env.add_filter("pluralize", pluralize); for (num, s) in [ (0, "You have 0 messages."), (1, "You have 1 message."), (10, "You have 10 messages."), ] { assert_eq!( env.render_str( "You have {{ num_messages }} message{{ num_messages|pluralize }}.", context! { num_messages => num, } ) .unwrap(), s ); } for (num, s) in [ (0, "You have 0 walruses."), (1, "You have 1 walrus."), (10, "You have 10 walruses."), ] { assert_eq!( env.render_str( r#"You have {{ num_walruses }} walrus{{ num_walruses|pluralize(None, "es") }}."#, context! { num_walruses => num, } ) .unwrap(), s ); } for (num, s) in [ (0, "You have 0 cherries."), (1, "You have 1 cherry."), (10, "You have 10 cherries."), ] { assert_eq!( env.render_str( r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#, context! { num_cherries => num, } ) .unwrap(), s ); } assert_eq!( env.render_str( r#"You have {{ num_cherries|length }} cherr{{ num_cherries|pluralize("y", "ies") }}."#, context! { num_cherries => vec![(); 5], } ) .unwrap(), "You have 5 cherries." ); assert_eq!( env.render_str( r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#, context! { num_cherries => 5, } ) .unwrap(), "You have 5 cherries." ); assert_eq!( env.render_str( r#"You have {{ num_cherries }} cherr{{ num_cherries|pluralize("y", "ies") }}."#, context! { num_cherries => 0.5f32, } ) .unwrap_err() .to_string(), "invalid operation: Pluralize argument is not an integer, or a sequence / object with \ a length but of type number (in :1)", ); } #[test] #[cfg(feature = "rand")] #[cfg(target_pointer_width = "64")] fn test_random() { // The small rng is pointer size specific. Test on 64bit platforms only use minijinja::render; use minijinja_contrib::filters::random; let mut env = Environment::new(); env.add_filter("random", random); insta::assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ [1, 2, 3, 4]|random }}"), @"1"); insta::assert_snapshot!(render!(in env, r"{% set RAND_SEED = 42 %}{{ 'HelloWorld'|random }}"), @"H"); } #[test] fn test_filesizeformat() { use minijinja::render; use minijinja_contrib::filters::filesizeformat; let mut env = Environment::new(); env.add_filter("filesizeformat", filesizeformat); insta::assert_snapshot!(render!(in env, r"{{ 0.5|filesizeformat }}"), @"0.5 Bytes"); insta::assert_snapshot!(render!(in env, r"{{ 1|filesizeformat }}"), @"1 Byte"); insta::assert_snapshot!(render!(in env, r"{{ -1|filesizeformat }}"), @"-1 Bytes"); insta::assert_snapshot!(render!(in env, r"{{ 1024|filesizeformat }}"), @"1.0 kB"); insta::assert_snapshot!(render!(in env, r"{{ 1024|filesizeformat(true) }}"), @"1.0 KiB"); insta::assert_snapshot!(render!(in env, r"{{ 1000|filesizeformat }}"), @"1.0 kB"); insta::assert_snapshot!(render!(in env, r"{{ 1000|filesizeformat(true) }}"), @"1000 Bytes"); insta::assert_snapshot!(render!(in env, r"{{ (1024 * 1024 * 1024)|filesizeformat }}"), @"1.1 GB"); insta::assert_snapshot!(render!(in env, r"{{ (1024 * 1024 * 1024)|filesizeformat(true) }}"), @"1.0 GiB"); insta::assert_snapshot!(render!(in env, r"{{ (1024 * 1024 * 1024 * 1024 * 1024)|filesizeformat }}"), @"1.1 PB"); insta::assert_snapshot!(render!(in env, r"{{ (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)|filesizeformat }}"), @"1.2 YB"); insta::assert_snapshot!(render!(in env, r"{{ (1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024)|filesizeformat }}"), @"1267650.6 YB"); } #[test] fn test_truncate() { use minijinja::render; use minijinja_contrib::filters::truncate; const LONG_TEXT: &str = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; const SHORT_TEXT: &str = "Fifteen chars !"; const SPECIAL_TEXT: &str = "Hello 👋 World"; let mut env = Environment::new(); env.add_filter("truncate", truncate); insta::assert_snapshot!( render!(in env, r"{{ text|truncate }}", text=>SHORT_TEXT), @"Fifteen chars !" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate }}", text=>LONG_TEXT), @"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It..." ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10) }}", text=>LONG_TEXT), @"Lorem..." ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10, killwords=true) }}", text=>LONG_TEXT), @"Lorem I..." ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10, end='***') }}", text=>LONG_TEXT), @"Lorem***" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10, killwords=true, end='') }}", text=>LONG_TEXT), @"Lorem Ipsu" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10, leeway=5) }}", text=>SHORT_TEXT), @"Fifteen chars !" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=10, leeway=0) }}", text=>SHORT_TEXT), @"Fifteen..." ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=7, leeway=0, end='') }}", text=>SPECIAL_TEXT), @"Hello" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=7, leeway=0, end='', killwords=true) }}", text=>SPECIAL_TEXT), @"Hello 👋" ); insta::assert_snapshot!( render!(in env, r"{{ text|truncate(length=8, leeway=0, end='') }}", text=>SPECIAL_TEXT), @"Hello 👋" ); assert_eq!( env.render_str(r"{{ 'hello'|truncate(length=1) }}", context! {}) .unwrap_err() .to_string(), "invalid operation: expected length >= 3, got 1 (in :1)" ); } #[test] #[cfg(feature = "wordwrap")] fn test_wordcount() { use minijinja_contrib::filters::wordcount; let mut env = Environment::new(); env.add_filter("wordcount", wordcount); assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "Hello world! How are you?" } ) .unwrap(), "5" ); // Test empty string assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "" } ) .unwrap(), "0" ); // Test multiple whitespace assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "Hello world! Test" } ) .unwrap(), "3" ); // Test other word separators assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "hello-again@world! It's_me!" } ) .unwrap(), "5" ); // Test multiple other word separators assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "hello--again@-!world" } ) .unwrap(), "3" ); // Test unicode marks assert_eq!( env.render_str( "{{ text|wordcount }}", context! { text => "helloाworld" } ) .unwrap(), "2" ); } #[test] #[cfg(feature = "wordwrap")] fn test_wordwrap() { use minijinja_contrib::filters::wordwrap; let mut env = minijinja::Environment::new(); env.add_filter("wordwrap", wordwrap); // Test basic wrapping assert_eq!( env.render_str( "{{ text|wordwrap(width=20) }}", context! { text => "This is a long piece of text that should be wrapped at a specific width." } ) .unwrap(), "This is a long piece\nof text that should\nbe wrapped at a\nspecific width." ); // Test custom wrap string assert_eq!( env.render_str( "{{ text|wordwrap(width=10, wrapstring='
') }}", context! { text => "This is a test of custom wrap strings." } ) .unwrap(), "This is
a test
of custom
wrap
strings." ); // Test preserving newlines assert_eq!( env.render_str( "{{ text|wordwrap(width=20) }}", context! { text => "First paragraph.\n\nSecond paragraph." } ) .unwrap(), "First paragraph.\n\nSecond paragraph." ); // Test breaking long words assert_eq!( env.render_str( "{{ text|wordwrap(width=10, break_long_words=true) }}", context! { text => "ThisIsAVeryLongWordThatShouldBeBroken" } ) .unwrap(), "ThisIsAVer\nyLongWordT\nhatShouldB\neBroken" ); // Test not breaking long words assert_eq!( env.render_str( "{{ text|wordwrap(width=10, break_long_words=false) }}", context! { text => "ThisIsAVeryLongWordThatShouldBeBroken" } ) .unwrap(), "ThisIsAVeryLongWordThatShouldBeBroken" ); // Test breaking on hyphens assert_eq!( env.render_str( "{{ text|wordwrap(width=10, break_on_hyphens=true) }}", context! { text => "This-is-a-hyphenated-word" } ) .unwrap(), "This-is-a-\nhyphenated\n-word" ); // Test NOT breaking on hyphens assert_eq!( env.render_str( "{{ text|wordwrap(width=10, break_on_hyphens=false) }}", context! { text => "This-is-a-hyphenated-word" } ) .unwrap(), "This-is-a-hyphenated-word" ); } #[test] fn test_striptags() { assert_eq!(striptags(" Hello ".into()), "Hello"); assert_eq!(striptags("Hello & World!>".into()), "Hello & World!>"); assert_eq!( striptags("Hello & World!>".into()), "Hello & World!>" ); assert_eq!(striptags("Hello &< World!>".into()), "Hello &"); assert_eq!( striptags("haha".into()), "haha" ); #[cfg(feature = "html_entities")] { assert_eq!(striptags("Hello Wörld".into()), "Hello Wörld"); assert_eq!(striptags(" \n  Hello \n ".into()), "Hello"); assert_eq!(striptags("  Hello \n ".into()), "Hello"); assert_eq!(striptags("a b".into()), "a b"); } assert_eq!(striptags("This is &&& x".into()), "This is &&& x"); assert_eq!( striptags("This is &unknownb".into()), "This is &unknownb" ); assert_eq!(striptags("This is &unknown".into()), "This is &unknown"); assert_eq!(striptags("This is &unknown;".into()), "This is &unknown;"); assert_eq!(striptags("This is &unknown x".into()), "This is &unknown x"); assert_eq!( striptags("This is &unknown; x".into()), "This is &unknown; x" ); } python-minijinja-2.12.0/minijinja-contrib/Cargo.toml0000664000175000017500000000270315052560140022303 0ustar carstencarsten[package] name = "minijinja-contrib" version = "2.12.0" edition = "2021" license = "Apache-2.0" authors = ["Armin Ronacher "] description = "Extra utilities for MiniJinja" homepage = "https://github.com/mitsuhiko/minijinja" repository = "https://github.com/mitsuhiko/minijinja" keywords = ["jinja", "jinja2", "templates"] readme = "README.md" rust-version = "1.70" [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "doc-header.html"] all-features = true [features] default = [] pycompat = ["minijinja/builtins"] datetime = ["time"] timezone = ["time-tz"] rand = [] html_entities = [] wordcount = ["unicode_categories"] wordwrap = ["textwrap"] unicode_wordwrap = ["wordwrap", "textwrap/unicode-linebreak", "textwrap/unicode-width"] [dependencies] minijinja = { version = "2.12.0", path = "../minijinja", default-features = false } serde = "1.0.164" textwrap = { version = "0.16.2", optional = true, default-features = false, features = ["smawk"] } time = { version = "0.3.35", optional = true, features = ["serde", "formatting", "parsing"] } time-tz = { version = "2.0.0", features = ["db"], optional = true } unicode_categories = { version = "0.1.1", optional = true } [dev-dependencies] insta = { version = "1.38.0", features = ["glob", "serde"] } chrono = { version = "0.4.26", features = ["serde"] } minijinja = { version = "2.12.0", path = "../minijinja", features = ["loader"] } similar-asserts = "1.4.2" python-minijinja-2.12.0/minijinja-contrib/LICENSE0000777000175000017500000000000015052560140022571 2../LICENSEustar carstencarstenpython-minijinja-2.12.0/minijinja-contrib/README.md0000664000175000017500000000211715052560140021631 0ustar carstencarsten# MiniJinja-Contrib [![License](https://img.shields.io/github/license/mitsuhiko/minijinja)](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) [![Crates.io](https://img.shields.io/crates/d/minijinja-contrib.svg)](https://crates.io/crates/minijinja-contrib) [![rustc 1.63.0](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg) [![Documentation](https://docs.rs/minijinja-contrib/badge.svg)](https://docs.rs/minijinja-contrib) MiniJinja-Contrib is a utility crate for [MiniJinja](https://github.com/mitsuhiko/minijinja) that adds support for certain utilities that are too specific for the MiniJinja core. This is usually because they provide functionality that Jinja2 itself does not have. ## Sponsor If you like the project and find it useful you can [become a sponsor](https://github.com/sponsors/mitsuhiko). ## License and Links - [Documentation](https://docs.rs/minijinja-contrib/) - [Issue Tracker](https://github.com/mitsuhiko/minijinja/issues) - License: [Apache-2.0](https://github.com/mitsuhiko/minijinja/blob/main/LICENSE) python-minijinja-2.12.0/minijinja-contrib/src/0000775000175000017500000000000015052560140021140 5ustar carstencarstenpython-minijinja-2.12.0/minijinja-contrib/src/filters/0000775000175000017500000000000015052560140022610 5ustar carstencarstenpython-minijinja-2.12.0/minijinja-contrib/src/filters/datetime.rs0000664000175000017500000003350315052560140024756 0ustar carstencarstenuse std::convert::TryFrom; use minijinja::value::{Kwargs, Value, ValueKind}; use minijinja::{Error, ErrorKind, State}; use serde::de::value::SeqDeserializer; use serde::Deserialize; use time::format_description::well_known::iso8601::Iso8601; use time::{format_description, Date, OffsetDateTime, PrimitiveDateTime}; fn handle_serde_error(err: serde::de::value::Error) -> Error { Error::new(ErrorKind::InvalidOperation, "not a valid date or timestamp").with_source(err) } #[allow(unused)] fn value_to_datetime( value: Value, state: &State, kwargs: &Kwargs, allow_date: bool, ) -> Result { #[allow(unused)] let mut timezone_already_handled = false; #[allow(unused_mut)] let (mut datetime, had_time) = if let Some(s) = value.as_str() { match OffsetDateTime::parse(s, &Iso8601::PARSING) { Ok(dt) => (dt, true), Err(original_err) => match PrimitiveDateTime::parse(s, &Iso8601::PARSING) { Ok(dt) => attach_timezone_to_primitive_datetime( state, kwargs, &mut timezone_already_handled, dt, )?, Err(_) => match Date::parse(s, &Iso8601::PARSING) { Ok(date) => (date.with_hms(0, 0, 0).unwrap().assume_utc(), false), Err(_) => { return Err(Error::new( ErrorKind::InvalidOperation, "not a valid date or timestamp", ) .with_source(original_err)) } }, }, } } else if let Ok(v) = f64::try_from(value.clone()) { ( OffsetDateTime::from_unix_timestamp_nanos((v * 1e9) as i128) .map_err(|_| Error::new(ErrorKind::InvalidOperation, "date out of range"))?, true, ) } else if value.kind() == ValueKind::Seq { let mut items = Vec::new(); for item in value.try_iter()? { items.push(i64::try_from(item)?); } if items.len() == 2 { ( Date::deserialize(SeqDeserializer::new(items.into_iter())) .map_err(handle_serde_error)? .with_hms(0, 0, 0) .unwrap() .assume_utc(), false, ) } else if items.len() == 6 { let dt = PrimitiveDateTime::deserialize(SeqDeserializer::new(items.into_iter())) .map_err(handle_serde_error)?; attach_timezone_to_primitive_datetime(state, kwargs, &mut timezone_already_handled, dt)? } else { ( OffsetDateTime::deserialize(SeqDeserializer::new(items.into_iter())) .map_err(handle_serde_error)?, true, ) } } else { return Err(Error::new( ErrorKind::InvalidOperation, "value is not a datetime", )); }; if had_time { #[cfg(feature = "timezone")] { if !timezone_already_handled { if let Some(tz) = get_timezone(state, kwargs)? { use time_tz::OffsetDateTimeExt; datetime = datetime.to_timezone(tz); } } } } else if !allow_date { return Err(Error::new( ErrorKind::InvalidOperation, "filter requires time, but only received a date", )); } Ok(datetime) } #[allow(unused)] fn attach_timezone_to_primitive_datetime( state: &State<'_, '_>, kwargs: &Kwargs, timezone_already_handled: &mut bool, dt: PrimitiveDateTime, ) -> Result<(OffsetDateTime, bool), Error> { #[cfg(feature = "timezone")] { if let Some(tz) = get_timezone(state, kwargs)? { *timezone_already_handled = true; Ok(( time_tz::PrimitiveDateTimeExt::assume_timezone(&dt, tz).unwrap_first(), true, )) } else { Ok((dt.assume_utc(), true)) } } #[cfg(not(feature = "timezone"))] { Ok((dt.assume_utc(), true)) } } #[cfg(feature = "timezone")] fn get_timezone( state: &State<'_, '_>, kwargs: &Kwargs, ) -> Result, Error> { let configured_tz = state.lookup("TIMEZONE"); let tzname = kwargs.get::>("tz")?.unwrap_or_else(|| { configured_tz .as_ref() .and_then(|x| x.as_str()) .unwrap_or("original") }); if tzname != "original" { Ok(Some(time_tz::timezones::get_by_name(tzname).ok_or_else( || { Error::new( ErrorKind::InvalidOperation, format!("unknown timezone '{tzname}'"), ) }, )?)) } else { Ok(None) } } /// Formats a timestamp as date and time. /// /// The value needs to be a unix timestamp, or a parsable string (ISO 8601) or a /// format supported by `chrono` or `time`. /// /// The filter accepts two keyword arguments (`format` and `tz`) to influence the format /// and the timezone. The default format is `"medium"`. The defaults for these keyword /// arguments are taken from two global variables in the template context: `DATETIME_FORMAT` /// and `TIMEZONE`. If the timezone is set to `"original"` or is not configured, then /// the timezone of the value is retained. Otherwise the timezone is the name of a /// timezone [from the database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). /// /// ```jinja /// {{ value|datetimeformat }} /// ``` /// /// ```jinja /// {{ value|datetimeformat(format="short") }} /// ``` /// /// ```jinja /// {{ value|datetimeformat(format="short", tz="Europe/Vienna") }} /// ``` /// /// This filter currently uses the `time` crate to format dates and uses the format /// string specification of that crate in version 2. For more information read the /// [Format description documentation](https://time-rs.github.io/book/api/format-description.html). /// Additionally some special formats are supported: /// /// * `short`: a short date and time format (`2023-06-24 16:37`) /// * `medium`: a medium length date and time format (`Jun 24 2023 16:37`) /// * `long`: a longer date and time format (`June 24 2023 16:37:22`) /// * `full`: a full date and time format (`Saturday, June 24 2023 16:37:22`) /// * `unix`: a unix timestamp in seconds only (`1687624642`) /// * `iso`: date and time in iso format (`2023-06-24T16:37:22+00:00`) /// /// This filter requires the `datetime` feature, the timezone support requires the `timezone` /// feature. #[cfg_attr(docsrs, doc(cfg(feature = "datetime")))] pub fn datetimeformat(state: &State, value: Value, kwargs: Kwargs) -> Result { let datetime = value_to_datetime(value, state, &kwargs, false)?; let configured_format = state.lookup("DATETIME_FORMAT"); let format = kwargs.get::>("format")?.unwrap_or_else(|| { configured_format .as_ref() .and_then(|x| x.as_str()) .unwrap_or("medium") }); kwargs.assert_all_used()?; datetime .format( &format_description::parse_borrowed::<2>(match format { "short" => "[year]-[month]-[day] [hour]:[minute]", "medium" => "[month repr:short] [day padding:none] [year] [hour]:[minute]", "long" => "[month repr:long] [day padding:none] [year] [hour]:[minute]:[second]", "full" => "[weekday], [month repr:long] [day padding:none] [year] [hour]:[minute]:[second].[subsecond]", "iso" => { "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]" } "unix" => "[unix_timestamp]", other => other, }) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "invalid format string").with_source(err) })?, ) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "failed to format date").with_source(err) }) } /// Formats a timestamp as time. /// /// The value needs to be a unix timestamp, or a parsable string (ISO 8601) or a /// format supported by `chrono` or `time`. /// /// The filter accepts two keyword arguments (`format` and `tz`) to influence the format /// and the timezone. The default format is `"medium"`. The defaults for these keyword /// arguments are taken from two global variables in the template context: `TIME_FORMAT` /// and `TIMEZONE`. If the timezone is set to `"original"` or is not configured, then /// the timezone of the value is retained. Otherwise the timezone is the name of a /// timezone [from the database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). /// /// ```jinja /// {{ value|timeformat }} /// ``` /// /// ```jinja /// {{ value|timeformat(format="short") }} /// ``` /// /// ```jinja /// {{ value|timeformat(format="short", tz="Europe/Vienna") }} /// ``` /// /// This filter currently uses the `time` crate to format dates and uses the format /// string specification of that crate in version 2. For more information read the /// [Format description documentation](https://time-rs.github.io/book/api/format-description.html). /// Additionally some special formats are supported: /// /// * `short` and `medium`: hour and minute (`16:37`) /// * `long`: includes seconds too (`16:37:22`) /// * `full`: includes subseconds too (`16:37:22.0`) /// * `unix`: a unix timestamp in seconds only (`1687624642`) /// * `iso`: date and time in iso format (`2023-06-24T16:37:22+00:00`) /// /// This filter requires the `datetime` feature, the timezone support requires the `timezone` /// feature. #[cfg_attr(docsrs, doc(cfg(feature = "datetime")))] pub fn timeformat(state: &State, value: Value, kwargs: Kwargs) -> Result { let datetime = value_to_datetime(value, state, &kwargs, false)?; let configured_format = state.lookup("TIME_FORMAT"); let format = kwargs.get::>("format")?.unwrap_or_else(|| { configured_format .as_ref() .and_then(|x| x.as_str()) .unwrap_or("medium") }); kwargs.assert_all_used()?; datetime .format( &format_description::parse_borrowed::<2>(match format { "short" | "medium" => "[hour]:[minute]", "long" => "[hour]:[minute]:[second]", "full" => "[hour]:[minute]:[second].[subsecond]", "iso" => { "[year]-[month]-[day]T[hour]:[minute]:[second][offset_hour sign:mandatory]:[offset_minute]" } "unix" => "[unix_timestamp]", other => other, }) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "invalid format string").with_source(err) })?, ) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "failed to format date").with_source(err) }) } /// Formats a timestamp as date. /// /// The value needs to be a unix timestamp, or a parsable string (ISO 8601) or a /// format supported by `chrono` or `time`. If the string does not include time /// information, then timezone adjustments are not performed. /// /// The filter accepts two keyword arguments (`format` and `tz`) to influence the format /// and the timezone. The default format is `"medium"`. The defaults for these keyword /// arguments are taken from two global variables in the template context: `DATE_FORMAT` /// and `TIMEZONE`. If the timezone is set to `"original"` or is not configured, then /// the timezone of the value is retained. Otherwise the timezone is the name of a /// timezone [from the database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). /// /// ```jinja /// {{ value|dateformat }} /// ``` /// /// ```jinja /// {{ value|dateformat(format="short") }} /// ``` /// /// ```jinja /// {{ value|dateformat(format="short", tz="Europe/Vienna") }} /// ``` /// /// This filter currently uses the `time` crate to format dates and uses the format /// string specification of that crate in version 2. For more information read the /// [Format description documentation](https://time-rs.github.io/book/api/format-description.html). /// Additionally some special formats are supported: /// /// * `short`: a short date format (`2023-06-24`) /// * `medium`: a medium length date format (`Jun 24 2023`) /// * `long`: a longer date format (`June 24 2023`) /// * `full`: a full date format (`Saturday, June 24 2023`) /// /// This filter requires the `datetime` feature, the timezone support requires the `timezone` /// feature. #[cfg_attr(docsrs, doc(cfg(feature = "datetime")))] pub fn dateformat(state: &State, value: Value, kwargs: Kwargs) -> Result { let datetime = value_to_datetime(value, state, &kwargs, true)?; let configured_format = state.lookup("DATE_FORMAT"); let format = kwargs.get::>("format")?.unwrap_or_else(|| { configured_format .as_ref() .and_then(|x| x.as_str()) .unwrap_or("medium") }); kwargs.assert_all_used()?; datetime .format( &format_description::parse_borrowed::<2>(match format { "short" => "[year]-[month]-[day]", "medium" => "[month repr:short] [day padding:none] [year]", "long" => "[month repr:long] [day padding:none] [year]", "full" => "[weekday], [month repr:long] [day padding:none] [year]", other => other, }) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "invalid format string").with_source(err) })?, ) .map_err(|err| { Error::new(ErrorKind::InvalidOperation, "failed to format date").with_source(err) }) } python-minijinja-2.12.0/minijinja-contrib/src/filters/mod.rs0000664000175000017500000003517415052560140023747 0ustar carstencarstenuse std::convert::TryFrom; use minijinja::value::{Kwargs, Value, ValueKind}; use minijinja::State; use minijinja::{Error, ErrorKind}; #[cfg(feature = "datetime")] mod datetime; #[cfg(feature = "datetime")] pub use self::datetime::*; #[cfg(feature = "html_entities")] use crate::html_entities::HTML_ENTITIES; // this list has to be ASCII sorted because we're going to binary search through it. #[cfg(not(feature = "html_entities"))] const HTML_ENTITIES: &[(&str, &str)] = &[("amp", "&"), ("gt", ">"), ("lt", "<"), ("quot", "\"")]; /// Returns a plural suffix if the value is not 1, '1', or an object of /// length 1. /// /// By default, the plural suffix is 's' and the singular suffix is /// empty (''). You can specify a singular suffix as the first argument (or /// `None`, for the default). You can specify a plural suffix as the second /// argument (or `None`, for the default). /// /// ```jinja /// {{ users|length }} user{{ users|pluralize }}. /// ``` /// /// ```jinja /// {{ entities|length }} entit{{ entities|pluralize("y", "ies") }}. /// ``` /// /// ```jinja /// {{ platypuses|length }} platypus{{ platypuses|pluralize(None, "es") }}. /// ``` pub fn pluralize( v: &Value, singular: Option, plural: Option, ) -> Result { let is_singular = match v.len() { Some(val) => val == 1, None => match i64::try_from(v.clone()) { Ok(val) => val == 1, Err(_) => { return Err(Error::new( ErrorKind::InvalidOperation, format!( "Pluralize argument is not an integer, or a sequence / object with a \ length but of type {}", v.kind() ), )); } }, }; let (rv, default) = if is_singular { (singular.unwrap_or(Value::UNDEFINED), "") } else { (plural.unwrap_or(Value::UNDEFINED), "s") }; if rv.is_undefined() || rv.is_none() { Ok(Value::from(default)) } else { Ok(rv) } } /// Chooses a random element from a sequence or string. /// /// The random number generated can be seeded with the `RAND_SEED` /// global context variable. /// /// ```jinja /// {{ [1, 2, 3, 4]|random }} /// ``` #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn random(state: &minijinja::State, seq: &Value) -> Result { use minijinja::value::ValueKind; if matches!(seq.kind(), ValueKind::Seq | ValueKind::String) { let len = seq.len().unwrap_or(0); let idx = crate::rand::XorShiftRng::for_state(state).next_usize(len); seq.get_item_by_index(idx) } else { Err(Error::new( ErrorKind::InvalidOperation, "can only select random elements from sequences", )) } } /// Formats the value like a "human-readable" file size. /// /// For example. 13 kB, 4.1 MB, 102 Bytes, etc. Per default decimal prefixes are /// used (Mega, Giga, etc.), if the second parameter is set to true /// the binary prefixes are used (Mebi, Gibi). pub fn filesizeformat(value: f64, binary: Option) -> String { const BIN_PREFIXES: &[&str] = &["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; const SI_PREFIXES: &[&str] = &["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; let (prefixes, base) = if binary.unwrap_or(false) { (BIN_PREFIXES, 1024.0) } else { (SI_PREFIXES, 1000.0) }; if value == 1.0 { return "1 Byte".into(); } let (sign, value) = if value < 0.0 { ("-", -value) } else { ("", value) }; if value < base { format!("{sign}{value} Bytes") } else { for (idx, prefix) in prefixes.iter().enumerate() { let unit = base.powf(idx as f64 + 2.0); if value < unit || idx == prefixes.len() - 1 { return format!("{}{:.1} {}", sign, base * value / unit, prefix); } } unreachable!(); } } /// Returns a truncated copy of the string. /// /// The string will be truncated to the specified length, with an ellipsis /// appended if truncation occurs. By default, the filter tries to preserve /// whole words. /// /// ```jinja /// {{ "Hello World"|truncate(length=5) }} /// ``` /// /// The filter accepts a few keyword arguments: /// * `length`: maximum length of the output string (defaults to 255) /// * `killwords`: set to `true` if you want to cut text exactly at length; if `false`, /// the filter will preserve last word (defaults to `false`) /// * `end`: if you want a specific ellipsis sign you can specify it (defaults to "...") /// * `leeway`: determines the tolerance margin before truncation occurs (defaults to 5) /// /// The truncation only occurs if the string length exceeds both the specified /// length and the leeway margin combined. This means that if a string is just /// slightly longer than the target length (within the leeway value), it will /// be left unmodified. /// /// When `killwords` is set to false (default behavior), the function ensures /// that words remain intact by finding the last complete word that fits within /// the length limit. This prevents words from being cut in the middle and /// maintains text readability. /// /// The specified length parameter is inclusive of the end string (ellipsis). /// For example, with a length of 5 and the default ellipsis "...", only 2 /// characters from the original string will be preserved. /// /// # Example with all attributes /// ```jinja /// {{ "Hello World"|truncate( /// length=5, /// killwords=true, /// end='...', /// leeway=2 /// ) }} /// ``` pub fn truncate(state: &State, value: &Value, kwargs: Kwargs) -> Result { if matches!(value.kind(), ValueKind::None | ValueKind::Undefined) { return Ok("".into()); } let s = value.as_str().ok_or_else(|| { Error::new( ErrorKind::InvalidOperation, format!("expected string, got {}", value.kind()), ) })?; let length = kwargs.get::>("length")?.unwrap_or(255); let killwords = kwargs.get::>("killwords")?.unwrap_or_default(); let end = kwargs.get::>("end")?.unwrap_or("..."); let leeway = kwargs.get::>("leeway")?.unwrap_or_else(|| { state .lookup("TRUNCATE_LEEWAY") .and_then(|x| usize::try_from(x.clone()).ok()) .unwrap_or(5) }); kwargs.assert_all_used()?; let end_len = end.chars().count(); if length < end_len { return Err(Error::new( ErrorKind::InvalidOperation, format!("expected length >= {end_len}, got {length}"), )); } if s.chars().count() <= length + leeway { return Ok(s.to_string()); } let trunc_pos = length - end_len; let truncated = if killwords { s.chars().take(trunc_pos).collect::() } else { let chars: Vec = s.chars().take(trunc_pos).collect(); match chars.iter().rposition(|&c| c == ' ') { Some(last_space) => chars[..last_space].iter().collect(), None => chars.iter().collect(), } }; let mut result = String::with_capacity(truncated.len() + end.len()); result.push_str(&truncated); result.push_str(end); Ok(result) } /// Counts the words in a string. /// /// ```jinja /// {{ "Hello world!"|wordcount }} /// ``` #[cfg(feature = "wordcount")] #[cfg_attr(docsrs, doc(cfg(feature = "wordcount")))] pub fn wordcount(value: &Value) -> Result { use unicode_categories::UnicodeCategories; let s = value.as_str().unwrap_or_default(); let mut count: u32 = 0; let mut in_word = false; // Iterate through characters, counting transitions from non-word to word chars for c in s.chars() { let is_word_char = c.is_letter() || c.is_numeric() || c == '_'; if is_word_char && !in_word { count += 1; in_word = true; } else if !is_word_char { in_word = false; } } Ok(Value::from(count)) } /// Wrap a string to the given width. /// /// By default this filter is not unicode aware (feature = `wordwrap`) but when the unicode /// feature is enabled (`unicode_wordwrap`) then it becomes so. It's implemented on top of /// the `textwrap` crate. /// /// **Keyword arguments:** /// /// - `width`: Maximum length of wrapped lines (default: 79) /// - `break_long_words`: If a word is longer than width, break it across lines (default: true) /// - `break_on_hyphens`: If a word contains hyphens, it may be split across lines (default: true) /// - `wrapstring`: String to join each wrapped line (default: newline) #[cfg(feature = "wordwrap")] #[cfg_attr(docsrs, doc(any(cfg(feature = "wordwrap"), cfg = "unicode_wordwrap")))] pub fn wordwrap(value: &Value, kwargs: Kwargs) -> Result { use textwrap::{wrap, Options as WrapOptions, WordSplitter}; let s = value.as_str().unwrap_or_default(); let width = kwargs.get::>("width")?.unwrap_or(79); let break_long_words = kwargs .get::>("break_long_words")? .unwrap_or(true); let break_on_hyphens = kwargs .get::>("break_on_hyphens")? .unwrap_or(true); let wrapstring = kwargs.get::>("wrapstring")?.unwrap_or("\n"); kwargs.assert_all_used()?; let mut options = WrapOptions::new(width); if break_on_hyphens { options = options .word_splitter(WordSplitter::HyphenSplitter) .break_words(break_long_words); } else { // When break_on_hyphens is false, we want to preserve hyphenated words entirely. // So we disable hyphen splitting and also disable breaking long words to ensure // hyphenated words stay together. options = options .word_splitter(WordSplitter::NoHyphenation) .break_words(false); } // Handle empty/whitespace-only input if s.trim().is_empty() { return Ok(Value::from("")); } // Process paragraphs sequentially into final string Ok(Value::from(s.lines().enumerate().fold( String::new(), |mut acc, (i, p)| { if i > 0 { acc.push_str(wrapstring); } if !p.trim().is_empty() { // Wrap the paragraph and join with wrapstring let wrapped = wrap(p, &options); for (j, line) in wrapped.iter().enumerate() { if j > 0 { acc.push_str(wrapstring); } acc.push_str(line); } } acc }, ))) } /// Performs HTML tag stripping and unescaping. /// /// ```jinja /// {{ "Hello & World"|striptags }} -> Hello & World /// ``` /// /// By default the filter only knows about `&`, `<`, `>`, and `&`. To /// get all HTML5 entities, you need to enable the `html_entities` feature. pub fn striptags(s: String) -> String { #[derive(Copy, Clone, PartialEq)] enum State { Text, TagStart, Tag, Entity, CommentStart1, CommentStart2, Comment, CommentEnd1, CommentEnd2, } let mut rv = String::new(); let mut entity_buffer = String::new(); let mut state = State::Text; macro_rules! push_char { ($c:expr) => { if $c.is_whitespace() { if rv.ends_with(|c: char| !c.is_whitespace()) { rv.push(' '); } } else { rv.push($c); } }; } for c in s.chars().map(Some).chain(Some(None)) { state = match (c, state) { (Some('<'), State::Text) => State::TagStart, (Some('>'), State::Tag | State::TagStart) => State::Text, (Some('!'), State::TagStart) => State::CommentStart1, (Some('-'), State::CommentStart1) => State::CommentStart2, (Some('-'), State::CommentStart2) => State::Comment, (_, State::CommentStart1 | State::CommentStart2) => State::Tag, (_, State::Tag | State::TagStart) => State::Tag, (Some('&'), State::Text) => { entity_buffer.clear(); State::Entity } (Some('-'), State::Comment) => State::CommentEnd1, (Some('-'), State::CommentEnd1) => State::CommentEnd2, (Some('>'), State::CommentEnd2) => State::Text, (_, State::CommentEnd1 | State::CommentEnd2) => State::Comment, (_, State::Entity) => { let cc = c.unwrap_or('\x00'); if cc == '\x00' || cc == ';' || cc == '<' || cc == '&' || cc.is_whitespace() { if let Some(resolved) = resolve_numeric_entity(&entity_buffer) { push_char!(resolved); } else if let Ok(resolved) = HTML_ENTITIES .binary_search_by_key(&entity_buffer.as_str(), |x| x.0) .map(|x| &HTML_ENTITIES[x].1) { for c in resolved.chars() { push_char!(c); } } else { rv.push('&'); rv.push_str(&entity_buffer); if cc == ';' { rv.push(';'); } } if cc == '<' { State::Tag } else if cc == '&' { entity_buffer.clear(); State::Entity } else { if cc.is_whitespace() { push_char!(cc); } State::Text } } else if let Some(c) = c { entity_buffer.push(c); State::Entity } else { State::Entity } } (Some(c), State::Text) => { push_char!(c); State::Text } (_, state) => state, } } rv.truncate(rv.trim_end().len()); rv } fn resolve_numeric_entity(entity: &str) -> Option { let num_str = entity.strip_prefix('#')?; if num_str.starts_with('x') || num_str.starts_with('X') { let code = u32::from_str_radix(&num_str[1..], 16).ok()?; char::from_u32(code) } else if let Ok(code) = num_str.parse::() { char::from_u32(code) } else { None } } #[test] fn test_entities_sorted() { assert!(HTML_ENTITIES.windows(2).all(|w| w[0] <= w[1])); } python-minijinja-2.12.0/minijinja-contrib/src/html_entities.rs0000664000175000017500000014174515052560140024372 0ustar carstencarsten// this list has to be ASCII sorted because we're going to binary search through it. pub const HTML_ENTITIES: &[(&str, &str)] = &[ ("AElig", "Æ"), ("AMP", "&"), ("Aacute", "Á"), ("Abreve", "Ă"), ("Acirc", "Â"), ("Acy", "А"), ("Afr", "𝔄"), ("Agrave", "À"), ("Alpha", "Α"), ("Amacr", "Ā"), ("And", "⩓"), ("Aogon", "Ą"), ("Aopf", "𝔸"), ("ApplyFunction", "⁡"), ("Aring", "Å"), ("Ascr", "𝒜"), ("Assign", "≔"), ("Atilde", "Ã"), ("Auml", "Ä"), ("Backslash", "∖"), ("Barv", "⫧"), ("Barwed", "⌆"), ("Bcy", "Б"), ("Because", "∵"), ("Bernoullis", "ℬ"), ("Beta", "Β"), ("Bfr", "𝔅"), ("Bopf", "𝔹"), ("Breve", "˘"), ("Bscr", "ℬ"), ("Bumpeq", "≎"), ("CHcy", "Ч"), ("COPY", "©"), ("Cacute", "Ć"), ("Cap", "⋒"), ("CapitalDifferentialD", "ⅅ"), ("Cayleys", "ℭ"), ("Ccaron", "Č"), ("Ccedil", "Ç"), ("Ccirc", "Ĉ"), ("Cconint", "∰"), ("Cdot", "Ċ"), ("Cedilla", "¸"), ("CenterDot", "·"), ("Cfr", "ℭ"), ("Chi", "Χ"), ("CircleDot", "⊙"), ("CircleMinus", "⊖"), ("CirclePlus", "⊕"), ("CircleTimes", "⊗"), ("ClockwiseContourIntegral", "∲"), ("CloseCurlyDoubleQuote", "”"), ("CloseCurlyQuote", "’"), ("Colon", "∷"), ("Colone", "⩴"), ("Congruent", "≡"), ("Conint", "∯"), ("ContourIntegral", "∮"), ("Copf", "ℂ"), ("Coproduct", "∐"), ("CounterClockwiseContourIntegral", "∳"), ("Cross", "⨯"), ("Cscr", "𝒞"), ("Cup", "⋓"), ("CupCap", "≍"), ("DD", "ⅅ"), ("DDotrahd", "⤑"), ("DJcy", "Ђ"), ("DScy", "Ѕ"), ("DZcy", "Џ"), ("Dagger", "‡"), ("Darr", "↡"), ("Dashv", "⫤"), ("Dcaron", "Ď"), ("Dcy", "Д"), ("Del", "∇"), ("Delta", "Δ"), ("Dfr", "𝔇"), ("DiacriticalAcute", "´"), ("DiacriticalDot", "˙"), ("DiacriticalDoubleAcute", "˝"), ("DiacriticalGrave", "`"), ("DiacriticalTilde", "˜"), ("Diamond", "⋄"), ("DifferentialD", "ⅆ"), ("Dopf", "𝔻"), ("Dot", "¨"), ("DotDot", "⃜"), ("DotEqual", "≐"), ("DoubleContourIntegral", "∯"), ("DoubleDot", "¨"), ("DoubleDownArrow", "⇓"), ("DoubleLeftArrow", "⇐"), ("DoubleLeftRightArrow", "⇔"), ("DoubleLeftTee", "⫤"), ("DoubleLongLeftArrow", "⟸"), ("DoubleLongLeftRightArrow", "⟺"), ("DoubleLongRightArrow", "⟹"), ("DoubleRightArrow", "⇒"), ("DoubleRightTee", "⊨"), ("DoubleUpArrow", "⇑"), ("DoubleUpDownArrow", "⇕"), ("DoubleVerticalBar", "∥"), ("DownArrow", "↓"), ("DownArrowBar", "⤓"), ("DownArrowUpArrow", "⇵"), ("DownBreve", "̑"), ("DownLeftRightVector", "⥐"), ("DownLeftTeeVector", "⥞"), ("DownLeftVector", "↽"), ("DownLeftVectorBar", "⥖"), ("DownRightTeeVector", "⥟"), ("DownRightVector", "⇁"), ("DownRightVectorBar", "⥗"), ("DownTee", "⊤"), ("DownTeeArrow", "↧"), ("Downarrow", "⇓"), ("Dscr", "𝒟"), ("Dstrok", "Đ"), ("ENG", "Ŋ"), ("ETH", "Ð"), ("Eacute", "É"), ("Ecaron", "Ě"), ("Ecirc", "Ê"), ("Ecy", "Э"), ("Edot", "Ė"), ("Efr", "𝔈"), ("Egrave", "È"), ("Element", "∈"), ("Emacr", "Ē"), ("EmptySmallSquare", "◻"), ("EmptyVerySmallSquare", "▫"), ("Eogon", "Ę"), ("Eopf", "𝔼"), ("Epsilon", "Ε"), ("Equal", "⩵"), ("EqualTilde", "≂"), ("Equilibrium", "⇌"), ("Escr", "ℰ"), ("Esim", "⩳"), ("Eta", "Η"), ("Euml", "Ë"), ("Exists", "∃"), ("ExponentialE", "ⅇ"), ("Fcy", "Ф"), ("Ffr", "𝔉"), ("FilledSmallSquare", "◼"), ("FilledVerySmallSquare", "▪"), ("Fopf", "𝔽"), ("ForAll", "∀"), ("Fouriertrf", "ℱ"), ("Fscr", "ℱ"), ("GJcy", "Ѓ"), ("GT", ">"), ("Gamma", "Γ"), ("Gammad", "Ϝ"), ("Gbreve", "Ğ"), ("Gcedil", "Ģ"), ("Gcirc", "Ĝ"), ("Gcy", "Г"), ("Gdot", "Ġ"), ("Gfr", "𝔊"), ("Gg", "⋙"), ("Gopf", "𝔾"), ("GreaterEqual", "≥"), ("GreaterEqualLess", "⋛"), ("GreaterFullEqual", "≧"), ("GreaterGreater", "⪢"), ("GreaterLess", "≷"), ("GreaterSlantEqual", "⩾"), ("GreaterTilde", "≳"), ("Gscr", "𝒢"), ("Gt", "≫"), ("HARDcy", "Ъ"), ("Hacek", "ˇ"), ("Hat", "^"), ("Hcirc", "Ĥ"), ("Hfr", "ℌ"), ("HilbertSpace", "ℋ"), ("Hopf", "ℍ"), ("HorizontalLine", "─"), ("Hscr", "ℋ"), ("Hstrok", "Ħ"), ("HumpDownHump", "≎"), ("HumpEqual", "≏"), ("IEcy", "Е"), ("IJlig", "IJ"), ("IOcy", "Ё"), ("Iacute", "Í"), ("Icirc", "Î"), ("Icy", "И"), ("Idot", "İ"), ("Ifr", "ℑ"), ("Igrave", "Ì"), ("Im", "ℑ"), ("Imacr", "Ī"), ("ImaginaryI", "ⅈ"), ("Implies", "⇒"), ("Int", "∬"), ("Integral", "∫"), ("Intersection", "⋂"), ("InvisibleComma", "⁣"), ("InvisibleTimes", "⁢"), ("Iogon", "Į"), ("Iopf", "𝕀"), ("Iota", "Ι"), ("Iscr", "ℐ"), ("Itilde", "Ĩ"), ("Iukcy", "І"), ("Iuml", "Ï"), ("Jcirc", "Ĵ"), ("Jcy", "Й"), ("Jfr", "𝔍"), ("Jopf", "𝕁"), ("Jscr", "𝒥"), ("Jsercy", "Ј"), ("Jukcy", "Є"), ("KHcy", "Х"), ("KJcy", "Ќ"), ("Kappa", "Κ"), ("Kcedil", "Ķ"), ("Kcy", "К"), ("Kfr", "𝔎"), ("Kopf", "𝕂"), ("Kscr", "𝒦"), ("LJcy", "Љ"), ("LT", "<"), ("Lacute", "Ĺ"), ("Lambda", "Λ"), ("Lang", "⟪"), ("Laplacetrf", "ℒ"), ("Larr", "↞"), ("Lcaron", "Ľ"), ("Lcedil", "Ļ"), ("Lcy", "Л"), ("LeftAngleBracket", "⟨"), ("LeftArrow", "←"), ("LeftArrowBar", "⇤"), ("LeftArrowRightArrow", "⇆"), ("LeftCeiling", "⌈"), ("LeftDoubleBracket", "⟦"), ("LeftDownTeeVector", "⥡"), ("LeftDownVector", "⇃"), ("LeftDownVectorBar", "⥙"), ("LeftFloor", "⌊"), ("LeftRightArrow", "↔"), ("LeftRightVector", "⥎"), ("LeftTee", "⊣"), ("LeftTeeArrow", "↤"), ("LeftTeeVector", "⥚"), ("LeftTriangle", "⊲"), ("LeftTriangleBar", "⧏"), ("LeftTriangleEqual", "⊴"), ("LeftUpDownVector", "⥑"), ("LeftUpTeeVector", "⥠"), ("LeftUpVector", "↿"), ("LeftUpVectorBar", "⥘"), ("LeftVector", "↼"), ("LeftVectorBar", "⥒"), ("Leftarrow", "⇐"), ("Leftrightarrow", "⇔"), ("LessEqualGreater", "⋚"), ("LessFullEqual", "≦"), ("LessGreater", "≶"), ("LessLess", "⪡"), ("LessSlantEqual", "⩽"), ("LessTilde", "≲"), ("Lfr", "𝔏"), ("Ll", "⋘"), ("Lleftarrow", "⇚"), ("Lmidot", "Ŀ"), ("LongLeftArrow", "⟵"), ("LongLeftRightArrow", "⟷"), ("LongRightArrow", "⟶"), ("Longleftarrow", "⟸"), ("Longleftrightarrow", "⟺"), ("Longrightarrow", "⟹"), ("Lopf", "𝕃"), ("LowerLeftArrow", "↙"), ("LowerRightArrow", "↘"), ("Lscr", "ℒ"), ("Lsh", "↰"), ("Lstrok", "Ł"), ("Lt", "≪"), ("Map", "⤅"), ("Mcy", "М"), ("MediumSpace", " "), ("Mellintrf", "ℳ"), ("Mfr", "𝔐"), ("MinusPlus", "∓"), ("Mopf", "𝕄"), ("Mscr", "ℳ"), ("Mu", "Μ"), ("NJcy", "Њ"), ("Nacute", "Ń"), ("Ncaron", "Ň"), ("Ncedil", "Ņ"), ("Ncy", "Н"), ("NegativeMediumSpace", "\u{200B}"), ("NegativeThickSpace", "\u{200B}"), ("NegativeThinSpace", "\u{200B}"), ("NegativeVeryThinSpace", "\u{200B}"), ("NestedGreaterGreater", "≫"), ("NestedLessLess", "≪"), ("NewLine", "\n"), ("Nfr", "𝔑"), ("NoBreak", "\u{2060}"), ("NonBreakingSpace", " "), ("Nopf", "ℕ"), ("Not", "⫬"), ("NotCongruent", "≢"), ("NotCupCap", "≭"), ("NotDoubleVerticalBar", "∦"), ("NotElement", "∉"), ("NotEqual", "≠"), ("NotEqualTilde", "≂̸"), ("NotExists", "∄"), ("NotGreater", "≯"), ("NotGreaterEqual", "≱"), ("NotGreaterFullEqual", "≧̸"), ("NotGreaterGreater", "≫̸"), ("NotGreaterLess", "≹"), ("NotGreaterSlantEqual", "⩾̸"), ("NotGreaterTilde", "≵"), ("NotHumpDownHump", "≎̸"), ("NotHumpEqual", "≏̸"), ("NotLeftTriangle", "⋪"), ("NotLeftTriangleBar", "⧏̸"), ("NotLeftTriangleEqual", "⋬"), ("NotLess", "≮"), ("NotLessEqual", "≰"), ("NotLessGreater", "≸"), ("NotLessLess", "≪̸"), ("NotLessSlantEqual", "⩽̸"), ("NotLessTilde", "≴"), ("NotNestedGreaterGreater", "⪢̸"), ("NotNestedLessLess", "⪡̸"), ("NotPrecedes", "⊀"), ("NotPrecedesEqual", "⪯̸"), ("NotPrecedesSlantEqual", "⋠"), ("NotReverseElement", "∌"), ("NotRightTriangle", "⋫"), ("NotRightTriangleBar", "⧐̸"), ("NotRightTriangleEqual", "⋭"), ("NotSquareSubset", "⊏̸"), ("NotSquareSubsetEqual", "⋢"), ("NotSquareSuperset", "⊐̸"), ("NotSquareSupersetEqual", "⋣"), ("NotSubset", "⊂⃒"), ("NotSubsetEqual", "⊈"), ("NotSucceeds", "⊁"), ("NotSucceedsEqual", "⪰̸"), ("NotSucceedsSlantEqual", "⋡"), ("NotSucceedsTilde", "≿̸"), ("NotSuperset", "⊃⃒"), ("NotSupersetEqual", "⊉"), ("NotTilde", "≁"), ("NotTildeEqual", "≄"), ("NotTildeFullEqual", "≇"), ("NotTildeTilde", "≉"), ("NotVerticalBar", "∤"), ("Nscr", "𝒩"), ("Ntilde", "Ñ"), ("Nu", "Ν"), ("OElig", "Œ"), ("Oacute", "Ó"), ("Ocirc", "Ô"), ("Ocy", "О"), ("Odblac", "Ő"), ("Ofr", "𝔒"), ("Ograve", "Ò"), ("Omacr", "Ō"), ("Omega", "Ω"), ("Omicron", "Ο"), ("Oopf", "𝕆"), ("OpenCurlyDoubleQuote", "“"), ("OpenCurlyQuote", "‘"), ("Or", "⩔"), ("Oscr", "𝒪"), ("Oslash", "Ø"), ("Otilde", "Õ"), ("Otimes", "⨷"), ("Ouml", "Ö"), ("OverBar", "‾"), ("OverBrace", "⏞"), ("OverBracket", "⎴"), ("OverParenthesis", "⏜"), ("PartialD", "∂"), ("Pcy", "П"), ("Pfr", "𝔓"), ("Phi", "Φ"), ("Pi", "Π"), ("PlusMinus", "±"), ("Poincareplane", "ℌ"), ("Popf", "ℙ"), ("Pr", "⪻"), ("Precedes", "≺"), ("PrecedesEqual", "⪯"), ("PrecedesSlantEqual", "≼"), ("PrecedesTilde", "≾"), ("Prime", "″"), ("Product", "∏"), ("Proportion", "∷"), ("Proportional", "∝"), ("Pscr", "𝒫"), ("Psi", "Ψ"), ("QUOT", "\""), ("Qfr", "𝔔"), ("Qopf", "ℚ"), ("Qscr", "𝒬"), ("RBarr", "⤐"), ("REG", "®"), ("Racute", "Ŕ"), ("Rang", "⟫"), ("Rarr", "↠"), ("Rarrtl", "⤖"), ("Rcaron", "Ř"), ("Rcedil", "Ŗ"), ("Rcy", "Р"), ("Re", "ℜ"), ("ReverseElement", "∋"), ("ReverseEquilibrium", "⇋"), ("ReverseUpEquilibrium", "⥯"), ("Rfr", "ℜ"), ("Rho", "Ρ"), ("RightAngleBracket", "⟩"), ("RightArrow", "→"), ("RightArrowBar", "⇥"), ("RightArrowLeftArrow", "⇄"), ("RightCeiling", "⌉"), ("RightDoubleBracket", "⟧"), ("RightDownTeeVector", "⥝"), ("RightDownVector", "⇂"), ("RightDownVectorBar", "⥕"), ("RightFloor", "⌋"), ("RightTee", "⊢"), ("RightTeeArrow", "↦"), ("RightTeeVector", "⥛"), ("RightTriangle", "⊳"), ("RightTriangleBar", "⧐"), ("RightTriangleEqual", "⊵"), ("RightUpDownVector", "⥏"), ("RightUpTeeVector", "⥜"), ("RightUpVector", "↾"), ("RightUpVectorBar", "⥔"), ("RightVector", "⇀"), ("RightVectorBar", "⥓"), ("Rightarrow", "⇒"), ("Ropf", "ℝ"), ("RoundImplies", "⥰"), ("Rrightarrow", "⇛"), ("Rscr", "ℛ"), ("Rsh", "↱"), ("RuleDelayed", "⧴"), ("SHCHcy", "Щ"), ("SHcy", "Ш"), ("SOFTcy", "Ь"), ("Sacute", "Ś"), ("Sc", "⪼"), ("Scaron", "Š"), ("Scedil", "Ş"), ("Scirc", "Ŝ"), ("Scy", "С"), ("Sfr", "𝔖"), ("ShortDownArrow", "↓"), ("ShortLeftArrow", "←"), ("ShortRightArrow", "→"), ("ShortUpArrow", "↑"), ("Sigma", "Σ"), ("SmallCircle", "∘"), ("Sopf", "𝕊"), ("Sqrt", "√"), ("Square", "□"), ("SquareIntersection", "⊓"), ("SquareSubset", "⊏"), ("SquareSubsetEqual", "⊑"), ("SquareSuperset", "⊐"), ("SquareSupersetEqual", "⊒"), ("SquareUnion", "⊔"), ("Sscr", "𝒮"), ("Star", "⋆"), ("Sub", "⋐"), ("Subset", "⋐"), ("SubsetEqual", "⊆"), ("Succeeds", "≻"), ("SucceedsEqual", "⪰"), ("SucceedsSlantEqual", "≽"), ("SucceedsTilde", "≿"), ("SuchThat", "∋"), ("Sum", "∑"), ("Sup", "⋑"), ("Superset", "⊃"), ("SupersetEqual", "⊇"), ("Supset", "⋑"), ("THORN", "Þ"), ("TRADE", "™"), ("TSHcy", "Ћ"), ("TScy", "Ц"), ("Tab", " "), ("Tau", "Τ"), ("Tcaron", "Ť"), ("Tcedil", "Ţ"), ("Tcy", "Т"), ("Tfr", "𝔗"), ("Therefore", "∴"), ("Theta", "Θ"), ("ThickSpace", "  "), ("ThinSpace", " "), ("Tilde", "∼"), ("TildeEqual", "≃"), ("TildeFullEqual", "≅"), ("TildeTilde", "≈"), ("Topf", "𝕋"), ("TripleDot", "⃛"), ("Tscr", "𝒯"), ("Tstrok", "Ŧ"), ("Uacute", "Ú"), ("Uarr", "↟"), ("Uarrocir", "⥉"), ("Ubrcy", "Ў"), ("Ubreve", "Ŭ"), ("Ucirc", "Û"), ("Ucy", "У"), ("Udblac", "Ű"), ("Ufr", "𝔘"), ("Ugrave", "Ù"), ("Umacr", "Ū"), ("UnderBar", "_"), ("UnderBrace", "⏟"), ("UnderBracket", "⎵"), ("UnderParenthesis", "⏝"), ("Union", "⋃"), ("UnionPlus", "⊎"), ("Uogon", "Ų"), ("Uopf", "𝕌"), ("UpArrow", "↑"), ("UpArrowBar", "⤒"), ("UpArrowDownArrow", "⇅"), ("UpDownArrow", "↕"), ("UpEquilibrium", "⥮"), ("UpTee", "⊥"), ("UpTeeArrow", "↥"), ("Uparrow", "⇑"), ("Updownarrow", "⇕"), ("UpperLeftArrow", "↖"), ("UpperRightArrow", "↗"), ("Upsi", "ϒ"), ("Upsilon", "Υ"), ("Uring", "Ů"), ("Uscr", "𝒰"), ("Utilde", "Ũ"), ("Uuml", "Ü"), ("VDash", "⊫"), ("Vbar", "⫫"), ("Vcy", "В"), ("Vdash", "⊩"), ("Vdashl", "⫦"), ("Vee", "⋁"), ("Verbar", "‖"), ("Vert", "‖"), ("VerticalBar", "∣"), ("VerticalLine", "|"), ("VerticalSeparator", "❘"), ("VerticalTilde", "≀"), ("VeryThinSpace", " "), ("Vfr", "𝔙"), ("Vopf", "𝕍"), ("Vscr", "𝒱"), ("Vvdash", "⊪"), ("Wcirc", "Ŵ"), ("Wedge", "⋀"), ("Wfr", "𝔚"), ("Wopf", "𝕎"), ("Wscr", "𝒲"), ("Xfr", "𝔛"), ("Xi", "Ξ"), ("Xopf", "𝕏"), ("Xscr", "𝒳"), ("YAcy", "Я"), ("YIcy", "Ї"), ("YUcy", "Ю"), ("Yacute", "Ý"), ("Ycirc", "Ŷ"), ("Ycy", "Ы"), ("Yfr", "𝔜"), ("Yopf", "𝕐"), ("Yscr", "𝒴"), ("Yuml", "Ÿ"), ("ZHcy", "Ж"), ("Zacute", "Ź"), ("Zcaron", "Ž"), ("Zcy", "З"), ("Zdot", "Ż"), ("ZeroWidthSpace", "\u{200B}"), ("Zeta", "Ζ"), ("Zfr", "ℨ"), ("Zopf", "ℤ"), ("Zscr", "𝒵"), ("aacute", "á"), ("abreve", "ă"), ("ac", "∾"), ("acE", "∾̳"), ("acd", "∿"), ("acirc", "â"), ("acute", "´"), ("acy", "а"), ("aelig", "æ"), ("af", "⁡"), ("afr", "𝔞"), ("agrave", "à"), ("alefsym", "ℵ"), ("aleph", "ℵ"), ("alpha", "α"), ("amacr", "ā"), ("amalg", "⨿"), ("amp", "&"), ("and", "∧"), ("andand", "⩕"), ("andd", "⩜"), ("andslope", "⩘"), ("andv", "⩚"), ("ang", "∠"), ("ange", "⦤"), ("angle", "∠"), ("angmsd", "∡"), ("angmsdaa", "⦨"), ("angmsdab", "⦩"), ("angmsdac", "⦪"), ("angmsdad", "⦫"), ("angmsdae", "⦬"), ("angmsdaf", "⦭"), ("angmsdag", "⦮"), ("angmsdah", "⦯"), ("angrt", "∟"), ("angrtvb", "⊾"), ("angrtvbd", "⦝"), ("angsph", "∢"), ("angst", "Å"), ("angzarr", "⍼"), ("aogon", "ą"), ("aopf", "𝕒"), ("ap", "≈"), ("apE", "⩰"), ("apacir", "⩯"), ("ape", "≊"), ("apid", "≋"), ("apos", "'"), ("approx", "≈"), ("approxeq", "≊"), ("aring", "å"), ("ascr", "𝒶"), ("ast", "*"), ("asymp", "≈"), ("asympeq", "≍"), ("atilde", "ã"), ("auml", "ä"), ("awconint", "∳"), ("awint", "⨑"), ("bNot", "⫭"), ("backcong", "≌"), ("backepsilon", "϶"), ("backprime", "‵"), ("backsim", "∽"), ("backsimeq", "⋍"), ("barvee", "⊽"), ("barwed", "⌅"), ("barwedge", "⌅"), ("bbrk", "⎵"), ("bbrktbrk", "⎶"), ("bcong", "≌"), ("bcy", "б"), ("bdquo", "„"), ("becaus", "∵"), ("because", "∵"), ("bemptyv", "⦰"), ("bepsi", "϶"), ("bernou", "ℬ"), ("beta", "β"), ("beth", "ℶ"), ("between", "≬"), ("bfr", "𝔟"), ("bigcap", "⋂"), ("bigcirc", "◯"), ("bigcup", "⋃"), ("bigodot", "⨀"), ("bigoplus", "⨁"), ("bigotimes", "⨂"), ("bigsqcup", "⨆"), ("bigstar", "★"), ("bigtriangledown", "▽"), ("bigtriangleup", "△"), ("biguplus", "⨄"), ("bigvee", "⋁"), ("bigwedge", "⋀"), ("bkarow", "⤍"), ("blacklozenge", "⧫"), ("blacksquare", "▪"), ("blacktriangle", "▴"), ("blacktriangledown", "▾"), ("blacktriangleleft", "◂"), ("blacktriangleright", "▸"), ("blank", "␣"), ("blk12", "▒"), ("blk14", "░"), ("blk34", "▓"), ("block", "█"), ("bne", "=⃥"), ("bnequiv", "≡⃥"), ("bnot", "⌐"), ("bopf", "𝕓"), ("bot", "⊥"), ("bottom", "⊥"), ("bowtie", "⋈"), ("boxDL", "╗"), ("boxDR", "╔"), ("boxDl", "╖"), ("boxDr", "╓"), ("boxH", "═"), ("boxHD", "╦"), ("boxHU", "╩"), ("boxHd", "╤"), ("boxHu", "╧"), ("boxUL", "╝"), ("boxUR", "╚"), ("boxUl", "╜"), ("boxUr", "╙"), ("boxV", "║"), ("boxVH", "╬"), ("boxVL", "╣"), ("boxVR", "╠"), ("boxVh", "╫"), ("boxVl", "╢"), ("boxVr", "╟"), ("boxbox", "⧉"), ("boxdL", "╕"), ("boxdR", "╒"), ("boxdl", "┐"), ("boxdr", "┌"), ("boxh", "─"), ("boxhD", "╥"), ("boxhU", "╨"), ("boxhd", "┬"), ("boxhu", "┴"), ("boxminus", "⊟"), ("boxplus", "⊞"), ("boxtimes", "⊠"), ("boxuL", "╛"), ("boxuR", "╘"), ("boxul", "┘"), ("boxur", "└"), ("boxv", "│"), ("boxvH", "╪"), ("boxvL", "╡"), ("boxvR", "╞"), ("boxvh", "┼"), ("boxvl", "┤"), ("boxvr", "├"), ("bprime", "‵"), ("breve", "˘"), ("brvbar", "¦"), ("bscr", "𝒷"), ("bsemi", "⁏"), ("bsim", "∽"), ("bsime", "⋍"), ("bsol", "\\"), ("bsolb", "⧅"), ("bsolhsub", "⟈"), ("bull", "•"), ("bullet", "•"), ("bump", "≎"), ("bumpE", "⪮"), ("bumpe", "≏"), ("bumpeq", "≏"), ("cacute", "ć"), ("cap", "∩"), ("capand", "⩄"), ("capbrcup", "⩉"), ("capcap", "⩋"), ("capcup", "⩇"), ("capdot", "⩀"), ("caps", "∩︀"), ("caret", "⁁"), ("caron", "ˇ"), ("ccaps", "⩍"), ("ccaron", "č"), ("ccedil", "ç"), ("ccirc", "ĉ"), ("ccups", "⩌"), ("ccupssm", "⩐"), ("cdot", "ċ"), ("cedil", "¸"), ("cemptyv", "⦲"), ("cent", "¢"), ("centerdot", "·"), ("cfr", "𝔠"), ("chcy", "ч"), ("check", "✓"), ("checkmark", "✓"), ("chi", "χ"), ("cir", "○"), ("cirE", "⧃"), ("circ", "ˆ"), ("circeq", "≗"), ("circlearrowleft", "↺"), ("circlearrowright", "↻"), ("circledR", "®"), ("circledS", "Ⓢ"), ("circledast", "⊛"), ("circledcirc", "⊚"), ("circleddash", "⊝"), ("cire", "≗"), ("cirfnint", "⨐"), ("cirmid", "⫯"), ("cirscir", "⧂"), ("clubs", "♣"), ("clubsuit", "♣"), ("colon", ":"), ("colone", "≔"), ("coloneq", "≔"), ("comma", ","), ("commat", "@"), ("comp", "∁"), ("compfn", "∘"), ("complement", "∁"), ("complexes", "ℂ"), ("cong", "≅"), ("congdot", "⩭"), ("conint", "∮"), ("copf", "𝕔"), ("coprod", "∐"), ("copy", "©"), ("copysr", "℗"), ("crarr", "↵"), ("cross", "✗"), ("cscr", "𝒸"), ("csub", "⫏"), ("csube", "⫑"), ("csup", "⫐"), ("csupe", "⫒"), ("ctdot", "⋯"), ("cudarrl", "⤸"), ("cudarrr", "⤵"), ("cuepr", "⋞"), ("cuesc", "⋟"), ("cularr", "↶"), ("cularrp", "⤽"), ("cup", "∪"), ("cupbrcap", "⩈"), ("cupcap", "⩆"), ("cupcup", "⩊"), ("cupdot", "⊍"), ("cupor", "⩅"), ("cups", "∪︀"), ("curarr", "↷"), ("curarrm", "⤼"), ("curlyeqprec", "⋞"), ("curlyeqsucc", "⋟"), ("curlyvee", "⋎"), ("curlywedge", "⋏"), ("curren", "¤"), ("curvearrowleft", "↶"), ("curvearrowright", "↷"), ("cuvee", "⋎"), ("cuwed", "⋏"), ("cwconint", "∲"), ("cwint", "∱"), ("cylcty", "⌭"), ("dArr", "⇓"), ("dHar", "⥥"), ("dagger", "†"), ("daleth", "ℸ"), ("darr", "↓"), ("dash", "‐"), ("dashv", "⊣"), ("dbkarow", "⤏"), ("dblac", "˝"), ("dcaron", "ď"), ("dcy", "д"), ("dd", "ⅆ"), ("ddagger", "‡"), ("ddarr", "⇊"), ("ddotseq", "⩷"), ("deg", "°"), ("delta", "δ"), ("demptyv", "⦱"), ("dfisht", "⥿"), ("dfr", "𝔡"), ("dharl", "⇃"), ("dharr", "⇂"), ("diam", "⋄"), ("diamond", "⋄"), ("diamondsuit", "♦"), ("diams", "♦"), ("die", "¨"), ("digamma", "ϝ"), ("disin", "⋲"), ("div", "÷"), ("divide", "÷"), ("divideontimes", "⋇"), ("divonx", "⋇"), ("djcy", "ђ"), ("dlcorn", "⌞"), ("dlcrop", "⌍"), ("dollar", "$"), ("dopf", "𝕕"), ("dot", "˙"), ("doteq", "≐"), ("doteqdot", "≑"), ("dotminus", "∸"), ("dotplus", "∔"), ("dotsquare", "⊡"), ("doublebarwedge", "⌆"), ("downarrow", "↓"), ("downdownarrows", "⇊"), ("downharpoonleft", "⇃"), ("downharpoonright", "⇂"), ("drbkarow", "⤐"), ("drcorn", "⌟"), ("drcrop", "⌌"), ("dscr", "𝒹"), ("dscy", "ѕ"), ("dsol", "⧶"), ("dstrok", "đ"), ("dtdot", "⋱"), ("dtri", "▿"), ("dtrif", "▾"), ("duarr", "⇵"), ("duhar", "⥯"), ("dwangle", "⦦"), ("dzcy", "џ"), ("dzigrarr", "⟿"), ("eDDot", "⩷"), ("eDot", "≑"), ("eacute", "é"), ("easter", "⩮"), ("ecaron", "ě"), ("ecir", "≖"), ("ecirc", "ê"), ("ecolon", "≕"), ("ecy", "э"), ("edot", "ė"), ("ee", "ⅇ"), ("efDot", "≒"), ("efr", "𝔢"), ("eg", "⪚"), ("egrave", "è"), ("egs", "⪖"), ("egsdot", "⪘"), ("el", "⪙"), ("elinters", "⏧"), ("ell", "ℓ"), ("els", "⪕"), ("elsdot", "⪗"), ("emacr", "ē"), ("empty", "∅"), ("emptyset", "∅"), ("emptyv", "∅"), ("emsp", " "), ("emsp13", " "), ("emsp14", " "), ("eng", "ŋ"), ("ensp", " "), ("eogon", "ę"), ("eopf", "𝕖"), ("epar", "⋕"), ("eparsl", "⧣"), ("eplus", "⩱"), ("epsi", "ε"), ("epsilon", "ε"), ("epsiv", "ϵ"), ("eqcirc", "≖"), ("eqcolon", "≕"), ("eqsim", "≂"), ("eqslantgtr", "⪖"), ("eqslantless", "⪕"), ("equals", "="), ("equest", "≟"), ("equiv", "≡"), ("equivDD", "⩸"), ("eqvparsl", "⧥"), ("erDot", "≓"), ("erarr", "⥱"), ("escr", "ℯ"), ("esdot", "≐"), ("esim", "≂"), ("eta", "η"), ("eth", "ð"), ("euml", "ë"), ("euro", "€"), ("excl", "!"), ("exist", "∃"), ("expectation", "ℰ"), ("exponentiale", "ⅇ"), ("fallingdotseq", "≒"), ("fcy", "ф"), ("female", "♀"), ("ffilig", "ffi"), ("fflig", "ff"), ("ffllig", "ffl"), ("ffr", "𝔣"), ("filig", "fi"), ("fjlig", "fj"), ("flat", "♭"), ("fllig", "fl"), ("fltns", "▱"), ("fnof", "ƒ"), ("fopf", "𝕗"), ("forall", "∀"), ("fork", "⋔"), ("forkv", "⫙"), ("fpartint", "⨍"), ("frac12", "½"), ("frac13", "⅓"), ("frac14", "¼"), ("frac15", "⅕"), ("frac16", "⅙"), ("frac18", "⅛"), ("frac23", "⅔"), ("frac25", "⅖"), ("frac34", "¾"), ("frac35", "⅗"), ("frac38", "⅜"), ("frac45", "⅘"), ("frac56", "⅚"), ("frac58", "⅝"), ("frac78", "⅞"), ("frasl", "⁄"), ("frown", "⌢"), ("fscr", "𝒻"), ("gE", "≧"), ("gEl", "⪌"), ("gacute", "ǵ"), ("gamma", "γ"), ("gammad", "ϝ"), ("gap", "⪆"), ("gbreve", "ğ"), ("gcirc", "ĝ"), ("gcy", "г"), ("gdot", "ġ"), ("ge", "≥"), ("gel", "⋛"), ("geq", "≥"), ("geqq", "≧"), ("geqslant", "⩾"), ("ges", "⩾"), ("gescc", "⪩"), ("gesdot", "⪀"), ("gesdoto", "⪂"), ("gesdotol", "⪄"), ("gesl", "⋛︀"), ("gesles", "⪔"), ("gfr", "𝔤"), ("gg", "≫"), ("ggg", "⋙"), ("gimel", "ℷ"), ("gjcy", "ѓ"), ("gl", "≷"), ("glE", "⪒"), ("gla", "⪥"), ("glj", "⪤"), ("gnE", "≩"), ("gnap", "⪊"), ("gnapprox", "⪊"), ("gne", "⪈"), ("gneq", "⪈"), ("gneqq", "≩"), ("gnsim", "⋧"), ("gopf", "𝕘"), ("grave", "`"), ("gscr", "ℊ"), ("gsim", "≳"), ("gsime", "⪎"), ("gsiml", "⪐"), ("gt", ">"), ("gtcc", "⪧"), ("gtcir", "⩺"), ("gtdot", "⋗"), ("gtlPar", "⦕"), ("gtquest", "⩼"), ("gtrapprox", "⪆"), ("gtrarr", "⥸"), ("gtrdot", "⋗"), ("gtreqless", "⋛"), ("gtreqqless", "⪌"), ("gtrless", "≷"), ("gtrsim", "≳"), ("gvertneqq", "≩︀"), ("gvnE", "≩︀"), ("hArr", "⇔"), ("hairsp", " "), ("half", "½"), ("hamilt", "ℋ"), ("hardcy", "ъ"), ("harr", "↔"), ("harrcir", "⥈"), ("harrw", "↭"), ("hbar", "ℏ"), ("hcirc", "ĥ"), ("hearts", "♥"), ("heartsuit", "♥"), ("hellip", "…"), ("hercon", "⊹"), ("hfr", "𝔥"), ("hksearow", "⤥"), ("hkswarow", "⤦"), ("hoarr", "⇿"), ("homtht", "∻"), ("hookleftarrow", "↩"), ("hookrightarrow", "↪"), ("hopf", "𝕙"), ("horbar", "―"), ("hscr", "𝒽"), ("hslash", "ℏ"), ("hstrok", "ħ"), ("hybull", "⁃"), ("hyphen", "‐"), ("iacute", "í"), ("ic", "⁣"), ("icirc", "î"), ("icy", "и"), ("iecy", "е"), ("iexcl", "¡"), ("iff", "⇔"), ("ifr", "𝔦"), ("igrave", "ì"), ("ii", "ⅈ"), ("iiiint", "⨌"), ("iiint", "∭"), ("iinfin", "⧜"), ("iiota", "℩"), ("ijlig", "ij"), ("imacr", "ī"), ("image", "ℑ"), ("imagline", "ℐ"), ("imagpart", "ℑ"), ("imath", "ı"), ("imof", "⊷"), ("imped", "Ƶ"), ("in", "∈"), ("incare", "℅"), ("infin", "∞"), ("infintie", "⧝"), ("inodot", "ı"), ("int", "∫"), ("intcal", "⊺"), ("integers", "ℤ"), ("intercal", "⊺"), ("intlarhk", "⨗"), ("intprod", "⨼"), ("iocy", "ё"), ("iogon", "į"), ("iopf", "𝕚"), ("iota", "ι"), ("iprod", "⨼"), ("iquest", "¿"), ("iscr", "𝒾"), ("isin", "∈"), ("isinE", "⋹"), ("isindot", "⋵"), ("isins", "⋴"), ("isinsv", "⋳"), ("isinv", "∈"), ("it", "⁢"), ("itilde", "ĩ"), ("iukcy", "і"), ("iuml", "ï"), ("jcirc", "ĵ"), ("jcy", "й"), ("jfr", "𝔧"), ("jmath", "ȷ"), ("jopf", "𝕛"), ("jscr", "𝒿"), ("jsercy", "ј"), ("jukcy", "є"), ("kappa", "κ"), ("kappav", "ϰ"), ("kcedil", "ķ"), ("kcy", "к"), ("kfr", "𝔨"), ("kgreen", "ĸ"), ("khcy", "х"), ("kjcy", "ќ"), ("kopf", "𝕜"), ("kscr", "𝓀"), ("lAarr", "⇚"), ("lArr", "⇐"), ("lAtail", "⤛"), ("lBarr", "⤎"), ("lE", "≦"), ("lEg", "⪋"), ("lHar", "⥢"), ("lacute", "ĺ"), ("laemptyv", "⦴"), ("lagran", "ℒ"), ("lambda", "λ"), ("lang", "⟨"), ("langd", "⦑"), ("langle", "⟨"), ("lap", "⪅"), ("laquo", "«"), ("larr", "←"), ("larrb", "⇤"), ("larrbfs", "⤟"), ("larrfs", "⤝"), ("larrhk", "↩"), ("larrlp", "↫"), ("larrpl", "⤹"), ("larrsim", "⥳"), ("larrtl", "↢"), ("lat", "⪫"), ("latail", "⤙"), ("late", "⪭"), ("lates", "⪭︀"), ("lbarr", "⤌"), ("lbbrk", "❲"), ("lbrace", "{"), ("lbrack", "["), ("lbrke", "⦋"), ("lbrksld", "⦏"), ("lbrkslu", "⦍"), ("lcaron", "ľ"), ("lcedil", "ļ"), ("lceil", "⌈"), ("lcub", "{"), ("lcy", "л"), ("ldca", "⤶"), ("ldquo", "“"), ("ldquor", "„"), ("ldrdhar", "⥧"), ("ldrushar", "⥋"), ("ldsh", "↲"), ("le", "≤"), ("leftarrow", "←"), ("leftarrowtail", "↢"), ("leftharpoondown", "↽"), ("leftharpoonup", "↼"), ("leftleftarrows", "⇇"), ("leftrightarrow", "↔"), ("leftrightarrows", "⇆"), ("leftrightharpoons", "⇋"), ("leftrightsquigarrow", "↭"), ("leftthreetimes", "⋋"), ("leg", "⋚"), ("leq", "≤"), ("leqq", "≦"), ("leqslant", "⩽"), ("les", "⩽"), ("lescc", "⪨"), ("lesdot", "⩿"), ("lesdoto", "⪁"), ("lesdotor", "⪃"), ("lesg", "⋚︀"), ("lesges", "⪓"), ("lessapprox", "⪅"), ("lessdot", "⋖"), ("lesseqgtr", "⋚"), ("lesseqqgtr", "⪋"), ("lessgtr", "≶"), ("lesssim", "≲"), ("lfisht", "⥼"), ("lfloor", "⌊"), ("lfr", "𝔩"), ("lg", "≶"), ("lgE", "⪑"), ("lhard", "↽"), ("lharu", "↼"), ("lharul", "⥪"), ("lhblk", "▄"), ("ljcy", "љ"), ("ll", "≪"), ("llarr", "⇇"), ("llcorner", "⌞"), ("llhard", "⥫"), ("lltri", "◺"), ("lmidot", "ŀ"), ("lmoust", "⎰"), ("lmoustache", "⎰"), ("lnE", "≨"), ("lnap", "⪉"), ("lnapprox", "⪉"), ("lne", "⪇"), ("lneq", "⪇"), ("lneqq", "≨"), ("lnsim", "⋦"), ("loang", "⟬"), ("loarr", "⇽"), ("lobrk", "⟦"), ("longleftarrow", "⟵"), ("longleftrightarrow", "⟷"), ("longmapsto", "⟼"), ("longrightarrow", "⟶"), ("looparrowleft", "↫"), ("looparrowright", "↬"), ("lopar", "⦅"), ("lopf", "𝕝"), ("loplus", "⨭"), ("lotimes", "⨴"), ("lowast", "∗"), ("lowbar", "_"), ("loz", "◊"), ("lozenge", "◊"), ("lozf", "⧫"), ("lpar", "("), ("lparlt", "⦓"), ("lrarr", "⇆"), ("lrcorner", "⌟"), ("lrhar", "⇋"), ("lrhard", "⥭"), ("lrm", "‎"), ("lrtri", "⊿"), ("lsaquo", "‹"), ("lscr", "𝓁"), ("lsh", "↰"), ("lsim", "≲"), ("lsime", "⪍"), ("lsimg", "⪏"), ("lsqb", "["), ("lsquo", "‘"), ("lsquor", "‚"), ("lstrok", "ł"), ("lt", "<"), ("ltcc", "⪦"), ("ltcir", "⩹"), ("ltdot", "⋖"), ("lthree", "⋋"), ("ltimes", "⋉"), ("ltlarr", "⥶"), ("ltquest", "⩻"), ("ltrPar", "⦖"), ("ltri", "◃"), ("ltrie", "⊴"), ("ltrif", "◂"), ("lurdshar", "⥊"), ("luruhar", "⥦"), ("lvertneqq", "≨︀"), ("lvnE", "≨︀"), ("mDDot", "∺"), ("macr", "¯"), ("male", "♂"), ("malt", "✠"), ("maltese", "✠"), ("map", "↦"), ("mapsto", "↦"), ("mapstodown", "↧"), ("mapstoleft", "↤"), ("mapstoup", "↥"), ("marker", "▮"), ("mcomma", "⨩"), ("mcy", "м"), ("mdash", "—"), ("measuredangle", "∡"), ("mfr", "𝔪"), ("mho", "℧"), ("micro", "µ"), ("mid", "∣"), ("midast", "*"), ("midcir", "⫰"), ("middot", "·"), ("minus", "−"), ("minusb", "⊟"), ("minusd", "∸"), ("minusdu", "⨪"), ("mlcp", "⫛"), ("mldr", "…"), ("mnplus", "∓"), ("models", "⊧"), ("mopf", "𝕞"), ("mp", "∓"), ("mscr", "𝓂"), ("mstpos", "∾"), ("mu", "μ"), ("multimap", "⊸"), ("mumap", "⊸"), ("nGg", "⋙̸"), ("nGt", "≫⃒"), ("nGtv", "≫̸"), ("nLeftarrow", "⇍"), ("nLeftrightarrow", "⇎"), ("nLl", "⋘̸"), ("nLt", "≪⃒"), ("nLtv", "≪̸"), ("nRightarrow", "⇏"), ("nVDash", "⊯"), ("nVdash", "⊮"), ("nabla", "∇"), ("nacute", "ń"), ("nang", "∠⃒"), ("nap", "≉"), ("napE", "⩰̸"), ("napid", "≋̸"), ("napos", "ʼn"), ("napprox", "≉"), ("natur", "♮"), ("natural", "♮"), ("naturals", "ℕ"), ("nbsp", " "), ("nbump", "≎̸"), ("nbumpe", "≏̸"), ("ncap", "⩃"), ("ncaron", "ň"), ("ncedil", "ņ"), ("ncong", "≇"), ("ncongdot", "⩭̸"), ("ncup", "⩂"), ("ncy", "н"), ("ndash", "–"), ("ne", "≠"), ("neArr", "⇗"), ("nearhk", "⤤"), ("nearr", "↗"), ("nearrow", "↗"), ("nedot", "≐̸"), ("nequiv", "≢"), ("nesear", "⤨"), ("nesim", "≂̸"), ("nexist", "∄"), ("nexists", "∄"), ("nfr", "𝔫"), ("ngE", "≧̸"), ("nge", "≱"), ("ngeq", "≱"), ("ngeqq", "≧̸"), ("ngeqslant", "⩾̸"), ("nges", "⩾̸"), ("ngsim", "≵"), ("ngt", "≯"), ("ngtr", "≯"), ("nhArr", "⇎"), ("nharr", "↮"), ("nhpar", "⫲"), ("ni", "∋"), ("nis", "⋼"), ("nisd", "⋺"), ("niv", "∋"), ("njcy", "њ"), ("nlArr", "⇍"), ("nlE", "≦̸"), ("nlarr", "↚"), ("nldr", "‥"), ("nle", "≰"), ("nleftarrow", "↚"), ("nleftrightarrow", "↮"), ("nleq", "≰"), ("nleqq", "≦̸"), ("nleqslant", "⩽̸"), ("nles", "⩽̸"), ("nless", "≮"), ("nlsim", "≴"), ("nlt", "≮"), ("nltri", "⋪"), ("nltrie", "⋬"), ("nmid", "∤"), ("nopf", "𝕟"), ("not", "¬"), ("notin", "∉"), ("notinE", "⋹̸"), ("notindot", "⋵̸"), ("notinva", "∉"), ("notinvb", "⋷"), ("notinvc", "⋶"), ("notni", "∌"), ("notniva", "∌"), ("notnivb", "⋾"), ("notnivc", "⋽"), ("npar", "∦"), ("nparallel", "∦"), ("nparsl", "⫽⃥"), ("npart", "∂̸"), ("npolint", "⨔"), ("npr", "⊀"), ("nprcue", "⋠"), ("npre", "⪯̸"), ("nprec", "⊀"), ("npreceq", "⪯̸"), ("nrArr", "⇏"), ("nrarr", "↛"), ("nrarrc", "⤳̸"), ("nrarrw", "↝̸"), ("nrightarrow", "↛"), ("nrtri", "⋫"), ("nrtrie", "⋭"), ("nsc", "⊁"), ("nsccue", "⋡"), ("nsce", "⪰̸"), ("nscr", "𝓃"), ("nshortmid", "∤"), ("nshortparallel", "∦"), ("nsim", "≁"), ("nsime", "≄"), ("nsimeq", "≄"), ("nsmid", "∤"), ("nspar", "∦"), ("nsqsube", "⋢"), ("nsqsupe", "⋣"), ("nsub", "⊄"), ("nsubE", "⫅̸"), ("nsube", "⊈"), ("nsubset", "⊂⃒"), ("nsubseteq", "⊈"), ("nsubseteqq", "⫅̸"), ("nsucc", "⊁"), ("nsucceq", "⪰̸"), ("nsup", "⊅"), ("nsupE", "⫆̸"), ("nsupe", "⊉"), ("nsupset", "⊃⃒"), ("nsupseteq", "⊉"), ("nsupseteqq", "⫆̸"), ("ntgl", "≹"), ("ntilde", "ñ"), ("ntlg", "≸"), ("ntriangleleft", "⋪"), ("ntrianglelefteq", "⋬"), ("ntriangleright", "⋫"), ("ntrianglerighteq", "⋭"), ("nu", "ν"), ("num", "#"), ("numero", "№"), ("numsp", " "), ("nvDash", "⊭"), ("nvHarr", "⤄"), ("nvap", "≍⃒"), ("nvdash", "⊬"), ("nvge", "≥⃒"), ("nvgt", ">⃒"), ("nvinfin", "⧞"), ("nvlArr", "⤂"), ("nvle", "≤⃒"), ("nvlt", "<⃒"), ("nvltrie", "⊴⃒"), ("nvrArr", "⤃"), ("nvrtrie", "⊵⃒"), ("nvsim", "∼⃒"), ("nwArr", "⇖"), ("nwarhk", "⤣"), ("nwarr", "↖"), ("nwarrow", "↖"), ("nwnear", "⤧"), ("oS", "Ⓢ"), ("oacute", "ó"), ("oast", "⊛"), ("ocir", "⊚"), ("ocirc", "ô"), ("ocy", "о"), ("odash", "⊝"), ("odblac", "ő"), ("odiv", "⨸"), ("odot", "⊙"), ("odsold", "⦼"), ("oelig", "œ"), ("ofcir", "⦿"), ("ofr", "𝔬"), ("ogon", "˛"), ("ograve", "ò"), ("ogt", "⧁"), ("ohbar", "⦵"), ("ohm", "Ω"), ("oint", "∮"), ("olarr", "↺"), ("olcir", "⦾"), ("olcross", "⦻"), ("oline", "‾"), ("olt", "⧀"), ("omacr", "ō"), ("omega", "ω"), ("omicron", "ο"), ("omid", "⦶"), ("ominus", "⊖"), ("oopf", "𝕠"), ("opar", "⦷"), ("operp", "⦹"), ("oplus", "⊕"), ("or", "∨"), ("orarr", "↻"), ("ord", "⩝"), ("order", "ℴ"), ("orderof", "ℴ"), ("ordf", "ª"), ("ordm", "º"), ("origof", "⊶"), ("oror", "⩖"), ("orslope", "⩗"), ("orv", "⩛"), ("oscr", "ℴ"), ("oslash", "ø"), ("osol", "⊘"), ("otilde", "õ"), ("otimes", "⊗"), ("otimesas", "⨶"), ("ouml", "ö"), ("ovbar", "⌽"), ("par", "∥"), ("para", "¶"), ("parallel", "∥"), ("parsim", "⫳"), ("parsl", "⫽"), ("part", "∂"), ("pcy", "п"), ("percnt", "%"), ("period", "."), ("permil", "‰"), ("perp", "⊥"), ("pertenk", "‱"), ("pfr", "𝔭"), ("phi", "φ"), ("phiv", "ϕ"), ("phmmat", "ℳ"), ("phone", "☎"), ("pi", "π"), ("pitchfork", "⋔"), ("piv", "ϖ"), ("planck", "ℏ"), ("planckh", "ℎ"), ("plankv", "ℏ"), ("plus", "+"), ("plusacir", "⨣"), ("plusb", "⊞"), ("pluscir", "⨢"), ("plusdo", "∔"), ("plusdu", "⨥"), ("pluse", "⩲"), ("plusmn", "±"), ("plussim", "⨦"), ("plustwo", "⨧"), ("pm", "±"), ("pointint", "⨕"), ("popf", "𝕡"), ("pound", "£"), ("pr", "≺"), ("prE", "⪳"), ("prap", "⪷"), ("prcue", "≼"), ("pre", "⪯"), ("prec", "≺"), ("precapprox", "⪷"), ("preccurlyeq", "≼"), ("preceq", "⪯"), ("precnapprox", "⪹"), ("precneqq", "⪵"), ("precnsim", "⋨"), ("precsim", "≾"), ("prime", "′"), ("primes", "ℙ"), ("prnE", "⪵"), ("prnap", "⪹"), ("prnsim", "⋨"), ("prod", "∏"), ("profalar", "⌮"), ("profline", "⌒"), ("profsurf", "⌓"), ("prop", "∝"), ("propto", "∝"), ("prsim", "≾"), ("prurel", "⊰"), ("pscr", "𝓅"), ("psi", "ψ"), ("puncsp", " "), ("qfr", "𝔮"), ("qint", "⨌"), ("qopf", "𝕢"), ("qprime", "⁗"), ("qscr", "𝓆"), ("quaternions", "ℍ"), ("quatint", "⨖"), ("quest", "?"), ("questeq", "≟"), ("quot", "\""), ("rAarr", "⇛"), ("rArr", "⇒"), ("rAtail", "⤜"), ("rBarr", "⤏"), ("rHar", "⥤"), ("race", "∽̱"), ("racute", "ŕ"), ("radic", "√"), ("raemptyv", "⦳"), ("rang", "⟩"), ("rangd", "⦒"), ("range", "⦥"), ("rangle", "⟩"), ("raquo", "»"), ("rarr", "→"), ("rarrap", "⥵"), ("rarrb", "⇥"), ("rarrbfs", "⤠"), ("rarrc", "⤳"), ("rarrfs", "⤞"), ("rarrhk", "↪"), ("rarrlp", "↬"), ("rarrpl", "⥅"), ("rarrsim", "⥴"), ("rarrtl", "↣"), ("rarrw", "↝"), ("ratail", "⤚"), ("ratio", "∶"), ("rationals", "ℚ"), ("rbarr", "⤍"), ("rbbrk", "❳"), ("rbrace", "}"), ("rbrack", "]"), ("rbrke", "⦌"), ("rbrksld", "⦎"), ("rbrkslu", "⦐"), ("rcaron", "ř"), ("rcedil", "ŗ"), ("rceil", "⌉"), ("rcub", "}"), ("rcy", "р"), ("rdca", "⤷"), ("rdldhar", "⥩"), ("rdquo", "”"), ("rdquor", "”"), ("rdsh", "↳"), ("real", "ℜ"), ("realine", "ℛ"), ("realpart", "ℜ"), ("reals", "ℝ"), ("rect", "▭"), ("reg", "®"), ("rfisht", "⥽"), ("rfloor", "⌋"), ("rfr", "𝔯"), ("rhard", "⇁"), ("rharu", "⇀"), ("rharul", "⥬"), ("rho", "ρ"), ("rhov", "ϱ"), ("rightarrow", "→"), ("rightarrowtail", "↣"), ("rightharpoondown", "⇁"), ("rightharpoonup", "⇀"), ("rightleftarrows", "⇄"), ("rightleftharpoons", "⇌"), ("rightrightarrows", "⇉"), ("rightsquigarrow", "↝"), ("rightthreetimes", "⋌"), ("ring", "˚"), ("risingdotseq", "≓"), ("rlarr", "⇄"), ("rlhar", "⇌"), ("rlm", "\u{200f}"), ("rmoust", "⎱"), ("rmoustache", "⎱"), ("rnmid", "⫮"), ("roang", "⟭"), ("roarr", "⇾"), ("robrk", "⟧"), ("ropar", "⦆"), ("ropf", "𝕣"), ("roplus", "⨮"), ("rotimes", "⨵"), ("rpar", ")"), ("rpargt", "⦔"), ("rppolint", "⨒"), ("rrarr", "⇉"), ("rsaquo", "›"), ("rscr", "𝓇"), ("rsh", "↱"), ("rsqb", "]"), ("rsquo", "’"), ("rsquor", "’"), ("rthree", "⋌"), ("rtimes", "⋊"), ("rtri", "▹"), ("rtrie", "⊵"), ("rtrif", "▸"), ("rtriltri", "⧎"), ("ruluhar", "⥨"), ("rx", "℞"), ("sacute", "ś"), ("sbquo", "‚"), ("sc", "≻"), ("scE", "⪴"), ("scap", "⪸"), ("scaron", "š"), ("sccue", "≽"), ("sce", "⪰"), ("scedil", "ş"), ("scirc", "ŝ"), ("scnE", "⪶"), ("scnap", "⪺"), ("scnsim", "⋩"), ("scpolint", "⨓"), ("scsim", "≿"), ("scy", "с"), ("sdot", "⋅"), ("sdotb", "⊡"), ("sdote", "⩦"), ("seArr", "⇘"), ("searhk", "⤥"), ("searr", "↘"), ("searrow", "↘"), ("sect", "§"), ("semi", ";"), ("seswar", "⤩"), ("setminus", "∖"), ("setmn", "∖"), ("sext", "✶"), ("sfr", "𝔰"), ("sfrown", "⌢"), ("sharp", "♯"), ("shchcy", "щ"), ("shcy", "ш"), ("shortmid", "∣"), ("shortparallel", "∥"), ("shy", "\u{00ad}"), ("sigma", "σ"), ("sigmaf", "ς"), ("sigmav", "ς"), ("sim", "∼"), ("simdot", "⩪"), ("sime", "≃"), ("simeq", "≃"), ("simg", "⪞"), ("simgE", "⪠"), ("siml", "⪝"), ("simlE", "⪟"), ("simne", "≆"), ("simplus", "⨤"), ("simrarr", "⥲"), ("slarr", "←"), ("smallsetminus", "∖"), ("smashp", "⨳"), ("smeparsl", "⧤"), ("smid", "∣"), ("smile", "⌣"), ("smt", "⪪"), ("smte", "⪬"), ("smtes", "⪬︀"), ("softcy", "ь"), ("sol", "/"), ("solb", "⧄"), ("solbar", "⌿"), ("sopf", "𝕤"), ("spades", "♠"), ("spadesuit", "♠"), ("spar", "∥"), ("sqcap", "⊓"), ("sqcaps", "⊓︀"), ("sqcup", "⊔"), ("sqcups", "⊔︀"), ("sqsub", "⊏"), ("sqsube", "⊑"), ("sqsubset", "⊏"), ("sqsubseteq", "⊑"), ("sqsup", "⊐"), ("sqsupe", "⊒"), ("sqsupset", "⊐"), ("sqsupseteq", "⊒"), ("squ", "□"), ("square", "□"), ("squarf", "▪"), ("squf", "▪"), ("srarr", "→"), ("sscr", "𝓈"), ("ssetmn", "∖"), ("ssmile", "⌣"), ("sstarf", "⋆"), ("star", "☆"), ("starf", "★"), ("straightepsilon", "ϵ"), ("straightphi", "ϕ"), ("strns", "¯"), ("sub", "⊂"), ("subE", "⫅"), ("subdot", "⪽"), ("sube", "⊆"), ("subedot", "⫃"), ("submult", "⫁"), ("subnE", "⫋"), ("subne", "⊊"), ("subplus", "⪿"), ("subrarr", "⥹"), ("subset", "⊂"), ("subseteq", "⊆"), ("subseteqq", "⫅"), ("subsetneq", "⊊"), ("subsetneqq", "⫋"), ("subsim", "⫇"), ("subsub", "⫕"), ("subsup", "⫓"), ("succ", "≻"), ("succapprox", "⪸"), ("succcurlyeq", "≽"), ("succeq", "⪰"), ("succnapprox", "⪺"), ("succneqq", "⪶"), ("succnsim", "⋩"), ("succsim", "≿"), ("sum", "∑"), ("sung", "♪"), ("sup", "⊃"), ("sup1", "¹"), ("sup2", "²"), ("sup3", "³"), ("supE", "⫆"), ("supdot", "⪾"), ("supdsub", "⫘"), ("supe", "⊇"), ("supedot", "⫄"), ("suphsol", "⟉"), ("suphsub", "⫗"), ("suplarr", "⥻"), ("supmult", "⫂"), ("supnE", "⫌"), ("supne", "⊋"), ("supplus", "⫀"), ("supset", "⊃"), ("supseteq", "⊇"), ("supseteqq", "⫆"), ("supsetneq", "⊋"), ("supsetneqq", "⫌"), ("supsim", "⫈"), ("supsub", "⫔"), ("supsup", "⫖"), ("swArr", "⇙"), ("swarhk", "⤦"), ("swarr", "↙"), ("swarrow", "↙"), ("swnwar", "⤪"), ("szlig", "ß"), ("target", "⌖"), ("tau", "τ"), ("tbrk", "⎴"), ("tcaron", "ť"), ("tcedil", "ţ"), ("tcy", "т"), ("tdot", "⃛"), ("telrec", "⌕"), ("tfr", "𝔱"), ("there4", "∴"), ("therefore", "∴"), ("theta", "θ"), ("thetasym", "ϑ"), ("thetav", "ϑ"), ("thickapprox", "≈"), ("thicksim", "∼"), ("thinsp", " "), ("thkap", "≈"), ("thksim", "∼"), ("thorn", "þ"), ("tilde", "˜"), ("times", "×"), ("timesb", "⊠"), ("timesbar", "⨱"), ("timesd", "⨰"), ("tint", "∭"), ("toea", "⤨"), ("top", "⊤"), ("topbot", "⌶"), ("topcir", "⫱"), ("topf", "𝕥"), ("topfork", "⫚"), ("tosa", "⤩"), ("tprime", "‴"), ("trade", "™"), ("triangle", "▵"), ("triangledown", "▿"), ("triangleleft", "◃"), ("trianglelefteq", "⊴"), ("triangleq", "≜"), ("triangleright", "▹"), ("trianglerighteq", "⊵"), ("tridot", "◬"), ("trie", "≜"), ("triminus", "⨺"), ("triplus", "⨹"), ("trisb", "⧍"), ("tritime", "⨻"), ("trpezium", "⏢"), ("tscr", "𝓉"), ("tscy", "ц"), ("tshcy", "ћ"), ("tstrok", "ŧ"), ("twixt", "≬"), ("twoheadleftarrow", "↞"), ("twoheadrightarrow", "↠"), ("uArr", "⇑"), ("uHar", "⥣"), ("uacute", "ú"), ("uarr", "↑"), ("ubrcy", "ў"), ("ubreve", "ŭ"), ("ucirc", "û"), ("ucy", "у"), ("udarr", "⇅"), ("udblac", "ű"), ("udhar", "⥮"), ("ufisht", "⥾"), ("ufr", "𝔲"), ("ugrave", "ù"), ("uharl", "↿"), ("uharr", "↾"), ("uhblk", "▀"), ("ulcorn", "⌜"), ("ulcorner", "⌜"), ("ulcrop", "⌏"), ("ultri", "◸"), ("umacr", "ū"), ("uml", "¨"), ("uogon", "ų"), ("uopf", "𝕦"), ("uparrow", "↑"), ("updownarrow", "↕"), ("upharpoonleft", "↿"), ("upharpoonright", "↾"), ("uplus", "⊎"), ("upsi", "υ"), ("upsih", "ϒ"), ("upsilon", "υ"), ("upuparrows", "⇈"), ("urcorn", "⌝"), ("urcorner", "⌝"), ("urcrop", "⌎"), ("uring", "ů"), ("urtri", "◹"), ("uscr", "𝓊"), ("utdot", "⋰"), ("utilde", "ũ"), ("utri", "▵"), ("utrif", "▴"), ("uuarr", "⇈"), ("uuml", "ü"), ("uwangle", "⦧"), ("vArr", "⇕"), ("vBar", "⫨"), ("vBarv", "⫩"), ("vDash", "⊨"), ("vangrt", "⦜"), ("varepsilon", "ϵ"), ("varkappa", "ϰ"), ("varnothing", "∅"), ("varphi", "ϕ"), ("varpi", "ϖ"), ("varpropto", "∝"), ("varr", "↕"), ("varrho", "ϱ"), ("varsigma", "ς"), ("varsubsetneq", "⊊︀"), ("varsubsetneqq", "⫋︀"), ("varsupsetneq", "⊋︀"), ("varsupsetneqq", "⫌︀"), ("vartheta", "ϑ"), ("vartriangleleft", "⊲"), ("vartriangleright", "⊳"), ("vcy", "в"), ("vdash", "⊢"), ("vee", "∨"), ("veebar", "⊻"), ("veeeq", "≚"), ("vellip", "⋮"), ("verbar", "|"), ("vert", "|"), ("vfr", "𝔳"), ("vltri", "⊲"), ("vnsub", "⊂⃒"), ("vnsup", "⊃⃒"), ("vopf", "𝕧"), ("vprop", "∝"), ("vrtri", "⊳"), ("vscr", "𝓋"), ("vsubnE", "⫋︀"), ("vsubne", "⊊︀"), ("vsupnE", "⫌︀"), ("vsupne", "⊋︀"), ("vzigzag", "⦚"), ("wcirc", "ŵ"), ("wedbar", "⩟"), ("wedge", "∧"), ("wedgeq", "≙"), ("weierp", "℘"), ("wfr", "𝔴"), ("wopf", "𝕨"), ("wp", "℘"), ("wr", "≀"), ("wreath", "≀"), ("wscr", "𝓌"), ("xcap", "⋂"), ("xcirc", "◯"), ("xcup", "⋃"), ("xdtri", "▽"), ("xfr", "𝔵"), ("xhArr", "⟺"), ("xharr", "⟷"), ("xi", "ξ"), ("xlArr", "⟸"), ("xlarr", "⟵"), ("xmap", "⟼"), ("xnis", "⋻"), ("xodot", "⨀"), ("xopf", "𝕩"), ("xoplus", "⨁"), ("xotime", "⨂"), ("xrArr", "⟹"), ("xrarr", "⟶"), ("xscr", "𝓍"), ("xsqcup", "⨆"), ("xuplus", "⨄"), ("xutri", "△"), ("xvee", "⋁"), ("xwedge", "⋀"), ("yacute", "ý"), ("yacy", "я"), ("ycirc", "ŷ"), ("ycy", "ы"), ("yen", "¥"), ("yfr", "𝔶"), ("yicy", "ї"), ("yopf", "𝕪"), ("yscr", "𝓎"), ("yucy", "ю"), ("yuml", "ÿ"), ("zacute", "ź"), ("zcaron", "ž"), ("zcy", "з"), ("zdot", "ż"), ("zeetrf", "ℨ"), ("zeta", "ζ"), ("zfr", "𝔷"), ("zhcy", "ж"), ("zigrarr", "⇝"), ("zopf", "𝕫"), ("zscr", "𝓏"), ("zwj", "‍"), ("zwnj", "‌"), ]; python-minijinja-2.12.0/minijinja-contrib/src/rand.rs0000664000175000017500000000374615052560140022444 0ustar carstencarstenuse std::collections::hash_map::RandomState; use std::hash::{BuildHasher, Hasher}; use std::sync::Arc; use minijinja::value::{Object, ObjectRepr}; use minijinja::State; #[derive(Debug)] pub struct XorShiftRng { seed: seed_impl::Seed, } impl Object for XorShiftRng { fn repr(self: &Arc) -> ObjectRepr { ObjectRepr::Plain } } impl XorShiftRng { pub fn for_state(state: &State) -> Arc { state.get_or_set_temp_object("minijinja-contrib-rng", || { XorShiftRng::new( state .lookup("RAND_SEED") .and_then(|x| u64::try_from(x).ok()), ) }) } pub fn new(seed: Option) -> XorShiftRng { XorShiftRng { seed: seed_impl::Seed::new( seed.unwrap_or_else(|| RandomState::new().build_hasher().finish()), ), } } pub fn next(&self) -> u64 { let mut rv = seed_impl::load(&self.seed); rv ^= rv << 13; rv ^= rv >> 7; rv ^= rv << 17; seed_impl::store(&self.seed, rv); rv } pub fn next_usize(&self, max: usize) -> usize { (self.random() * max as f64) as usize } pub fn random(&self) -> f64 { (self.next() as f64) / (u64::MAX as f64) } pub fn random_range(&self, lower: i64, upper: i64) -> i64 { (self.random() * (upper - lower) as f64) as i64 + lower } } #[cfg(target_has_atomic = "64")] mod seed_impl { pub type Seed = std::sync::atomic::AtomicU64; pub fn load(seed: &Seed) -> u64 { seed.load(std::sync::atomic::Ordering::Relaxed) } pub fn store(seed: &Seed, v: u64) { seed.store(v, std::sync::atomic::Ordering::Relaxed); } } #[cfg(not(target_has_atomic = "64"))] mod seed_impl { pub type Seed = std::sync::Mutex; pub fn load(seed: &Seed) -> u64 { *seed.lock().unwrap() } pub fn store(seed: &Seed, v: u64) { *seed.lock().unwrap() = v; } } python-minijinja-2.12.0/minijinja-contrib/src/pycompat.rs0000664000175000017500000002623215052560140023347 0ustar carstencarstenuse minijinja::value::{from_args, ValueKind}; use minijinja::{Error, ErrorKind, State, Value}; /// An unknown method callback implementing python methods on primitives. /// /// This implements a lot of Python methods on basic types so that the /// compatibility with Jinja2 templates improves. /// /// ``` /// use minijinja::Environment; /// use minijinja_contrib::pycompat::unknown_method_callback; /// /// let mut env = Environment::new(); /// env.set_unknown_method_callback(unknown_method_callback); /// ``` /// /// Today the following methods are implemented: /// /// * `dict.get` /// * `dict.items` /// * `dict.keys` /// * `dict.values` /// * `list.count` /// * `str.capitalize` /// * `str.count` /// * `str.endswith` /// * `str.find` /// * `str.isalnum` /// * `str.isalpha` /// * `str.isascii` /// * `str.isdigit` /// * `str.islower` /// * `str.isnumeric` /// * `str.isupper` /// * `str.join` /// * `str.lower` /// * `str.lstrip` /// * `str.replace` /// * `str.rfind` /// * `str.rstrip` /// * `str.split` /// * `str.splitlines` /// * `str.startswith` /// * `str.strip` /// * `str.title` /// * `str.upper` #[cfg_attr(docsrs, doc(cfg(feature = "pycompat")))] pub fn unknown_method_callback( _state: &State, value: &Value, method: &str, args: &[Value], ) -> Result { match value.kind() { ValueKind::String => string_methods(value, method, args), ValueKind::Map => map_methods(value, method, args), ValueKind::Seq => seq_methods(value, method, args), _ => Err(Error::from(ErrorKind::UnknownMethod)), } } fn string_methods(value: &Value, method: &str, args: &[Value]) -> Result { let Some(s) = value.as_str() else { return Err(Error::from(ErrorKind::UnknownMethod)); }; match method { "upper" => { let () = from_args(args)?; Ok(Value::from(s.to_uppercase())) } "lower" => { let () = from_args(args)?; Ok(Value::from(s.to_lowercase())) } "islower" => { let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_lowercase()))) } "isupper" => { let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_uppercase()))) } "isspace" => { let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_whitespace()))) } "isdigit" | "isnumeric" => { // this is not a perfect mapping to what Python does, but // close enough for most uses in templates. let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_numeric()))) } "isalnum" => { let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_alphanumeric()))) } "isalpha" => { let () = from_args(args)?; Ok(Value::from(s.chars().all(|x| x.is_alphabetic()))) } "isascii" => { let () = from_args(args)?; Ok(Value::from(s.is_ascii())) } "strip" => { let (chars,): (Option<&str>,) = from_args(args)?; Ok(Value::from(if let Some(chars) = chars { s.trim_matches(&chars.chars().collect::>()[..]) } else { s.trim() })) } "lstrip" => { let (chars,): (Option<&str>,) = from_args(args)?; Ok(Value::from(if let Some(chars) = chars { s.trim_start_matches(&chars.chars().collect::>()[..]) } else { s.trim_start() })) } "rstrip" => { let (chars,): (Option<&str>,) = from_args(args)?; Ok(Value::from(if let Some(chars) = chars { s.trim_end_matches(&chars.chars().collect::>()[..]) } else { s.trim_end() })) } "replace" => { let (old, new, count): (&str, &str, Option) = from_args(args)?; let count = count.unwrap_or(-1); Ok(Value::from(if count < 0 { s.replace(old, new) } else { s.replacen(old, new, count as usize) })) } "title" => { let () = from_args(args)?; // one shall not call into these filters. However we consider ourselves // privileged. Ok(Value::from(minijinja::filters::title(s.into()))) } "split" => { let (sep, maxsplits) = from_args(args)?; // one shall not call into these filters. However we consider ourselves // privileged. Ok(minijinja::filters::split(s.into(), sep, maxsplits) .try_iter()? .collect::()) } "splitlines" => { let (keepends,): (Option,) = from_args(args)?; if !keepends.unwrap_or(false) { Ok(s.lines().map(Value::from).collect()) } else { let mut rv = Vec::new(); let mut rest = s; while let Some(offset) = rest.find('\n') { rv.push(Value::from(&rest[..offset + 1])); rest = &rest[offset + 1..]; } if !rest.is_empty() { rv.push(Value::from(rest)); } Ok(Value::from(rv)) } } "capitalize" => { let () = from_args(args)?; // one shall not call into these filters. However we consider ourselves // privileged. Ok(Value::from(minijinja::filters::capitalize(s.into()))) } "count" => { let (what,): (&str,) = from_args(args)?; let mut c = 0; let mut rest = s; while let Some(offset) = rest.find(what) { c += 1; rest = &rest[offset + what.len()..]; } Ok(Value::from(c)) } "find" => { let (what,): (&str,) = from_args(args)?; Ok(Value::from(match s.find(what) { Some(x) => x as i64, None => -1, })) } "rfind" => { let (what,): (&str,) = from_args(args)?; Ok(Value::from(match s.rfind(what) { Some(x) => x as i64, None => -1, })) } "startswith" => { let (prefix,): (&Value,) = from_args(args)?; if let Some(prefix) = prefix.as_str() { Ok(Value::from(s.starts_with(prefix))) } else if matches!(prefix.kind(), ValueKind::Iterable | ValueKind::Seq) { for prefix in prefix.try_iter()? { if s.starts_with(prefix.as_str().ok_or_else(|| { Error::new( ErrorKind::InvalidOperation, format!( "tuple for startswith must contain only strings, not {}", prefix.kind() ), ) })?) { return Ok(Value::from(true)); } } Ok(Value::from(false)) } else { Err(Error::new( ErrorKind::InvalidOperation, format!( "startswith argument must be string or a tuple of strings, not {}", prefix.kind() ), )) } } "endswith" => { let (suffix,): (&Value,) = from_args(args)?; if let Some(suffix) = suffix.as_str() { Ok(Value::from(s.ends_with(suffix))) } else if matches!(suffix.kind(), ValueKind::Iterable | ValueKind::Seq) { for suffix in suffix.try_iter()? { if s.ends_with(suffix.as_str().ok_or_else(|| { Error::new( ErrorKind::InvalidOperation, format!( "tuple for endswith must contain only strings, not {}", suffix.kind() ), ) })?) { return Ok(Value::from(true)); } } Ok(Value::from(false)) } else { Err(Error::new( ErrorKind::InvalidOperation, format!( "endswith argument must be string or a tuple of strings, not {}", suffix.kind() ), )) } } "join" => { use std::fmt::Write; let (values,): (&Value,) = from_args(args)?; let mut rv = String::new(); for (idx, value) in values.try_iter()?.enumerate() { if idx > 0 { rv.push_str(s); } write!(rv, "{value}").ok(); } Ok(Value::from(rv)) } _ => Err(Error::from(ErrorKind::UnknownMethod)), } } fn map_methods(value: &Value, method: &str, args: &[Value]) -> Result { let Some(obj) = value.as_object() else { return Err(Error::from(ErrorKind::UnknownMethod)); }; match method { "keys" => { let () = from_args(args)?; Ok(Value::make_object_iterable(obj.clone(), |obj| { match obj.try_iter() { Some(iter) => iter, None => Box::new(None.into_iter()), } })) } "values" => { let () = from_args(args)?; Ok(Value::make_object_iterable(obj.clone(), |obj| { match obj.try_iter_pairs() { Some(iter) => Box::new(iter.map(|(_, v)| v)), None => Box::new(None.into_iter()), } })) } "items" => { let () = from_args(args)?; Ok(Value::make_object_iterable(obj.clone(), |obj| { match obj.try_iter_pairs() { Some(iter) => Box::new(iter.map(|(k, v)| Value::from(vec![k, v]))), None => Box::new(None.into_iter()), } })) } "get" => { let (key,): (&Value,) = from_args(args)?; Ok(match obj.get_value(key) { Some(value) => value, None => Value::from(()), }) } _ => Err(Error::from(ErrorKind::UnknownMethod)), } } fn seq_methods(value: &Value, method: &str, args: &[Value]) -> Result { let Some(obj) = value.as_object() else { return Err(Error::from(ErrorKind::UnknownMethod)); }; match method { "count" => { let (what,): (&Value,) = from_args(args)?; Ok(Value::from(if let Some(iter) = obj.try_iter() { iter.filter(|x| x == what).count() } else { 0 })) } _ => Err(Error::from(ErrorKind::UnknownMethod)), } } python-minijinja-2.12.0/minijinja-contrib/src/globals.rs0000664000175000017500000002154215052560140023135 0ustar carstencarstenuse std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; #[allow(unused)] use minijinja::value::Value; use minijinja::value::{from_args, Object, ObjectRepr}; use minijinja::{Error, ErrorKind, State}; /// Returns the current time in UTC as unix timestamp. /// /// To format this timestamp, use the [`datetimeformat`](crate::filters::datetimeformat) filter. #[cfg(feature = "datetime")] #[cfg_attr(docsrs, doc(cfg(feature = "datetime")))] pub fn now() -> Value { let now = time::OffsetDateTime::now_utc(); Value::from(((now.unix_timestamp_nanos() / 1000) as f64) / 1_000_000.0) } /// Returns a cycler. /// /// Similar to `loop.cycle`, but can be used outside loops or across /// multiple loops. For example, render a list of folders and files in a /// list, alternating giving them "odd" and "even" classes. /// /// ```jinja /// {% set row_class = cycler("odd", "even") %} ///
    /// {% for folder in folders %} ///
  • {{ folder }} /// {% endfor %} /// {% for file in files %} ///
  • {{ file }} /// {% endfor %} ///
/// ``` pub fn cycler(items: Vec) -> Result { #[derive(Debug)] pub struct Cycler { items: Vec, pos: AtomicUsize, } impl Object for Cycler { fn repr(self: &Arc) -> ObjectRepr { ObjectRepr::Plain } fn call_method( self: &Arc, _state: &State<'_, '_>, method: &str, args: &[Value], ) -> Result { match method { "next" => { let () = from_args(args)?; let idx = self.pos.load(Ordering::Relaxed); self.pos .store((idx + 1) % self.items.len(), Ordering::Relaxed); Ok(self.items[idx].clone()) } _ => Err(Error::from(ErrorKind::UnknownMethod)), } } } if items.is_empty() { Err(Error::new( ErrorKind::InvalidOperation, "at least one value required", )) } else { Ok(Value::from_object(Cycler { items, pos: AtomicUsize::new(0), })) } } /// A tiny helper that can be used to “join” multiple sections. A /// joiner is passed a string and will return that string every time /// it’s called, except the first time (in which case it returns an /// empty string). You can use this to join things: /// /// ```jinja /// {% set pipe = joiner("|") %} /// {% if categories %} {{ pipe() }} /// Categories: {{ categories|join(", ") }} /// {% endif %} /// {% if author %} {{ pipe() }} /// Author: {{ author() }} /// {% endif %} /// {% if can_edit %} {{ pipe() }} ///
Edit /// {% endif %} /// ``` pub fn joiner(sep: Option) -> Value { #[derive(Debug)] struct Joiner { sep: Value, used: AtomicBool, } impl Object for Joiner { fn repr(self: &Arc) -> ObjectRepr { ObjectRepr::Plain } fn call(self: &Arc, _state: &State<'_, '_>, args: &[Value]) -> Result { let () = from_args(args)?; let used = self.used.swap(true, Ordering::Relaxed); if used { Ok(self.sep.clone()) } else { Ok(Value::from("")) } } } Value::from_object(Joiner { sep: sep.unwrap_or_else(|| Value::from(", ")), used: AtomicBool::new(false), }) } /// Returns a random number in a given range. /// /// If only one parameter is provided it's taken as exclusive upper /// bound with 0 as lower bound, otherwise two parameters need to be /// passed for the lower and upper bound. Only integers are permitted. /// /// The random number generated can be seeded with the `RAND_SEED` /// global context variable. #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn randrange(state: &State, n: i64, m: Option) -> i64 { let (lower, upper) = match m { None => (0, n), Some(m) => (n, m), }; crate::rand::XorShiftRng::for_state(state).random_range(lower, upper) } /// Generates a random lorem ipsum. /// /// The random number generated can be seeded with the `RAND_SEED` /// global context variable. /// /// The function accepts various keyword arguments: /// /// * `n`: number of paragraphs to generate. /// * `min`: minimum number of words to generate per paragraph. /// * `max`: maximum number of words to generate per paragraph. /// * `html`: set to `true` to generate HTML paragraphs instead. #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn lipsum( state: &State, n: Option, kwargs: minijinja::value::Kwargs, ) -> Result { #[rustfmt::skip] const LIPSUM_WORDS: &[&str] = &[ "a", "ac", "accumsan", "ad", "adipiscing", "aenean", "aliquam", "aliquet", "amet", "ante", "aptent", "arcu", "at", "auctor", "augue", "bibendum", "blandit", "class", "commodo", "condimentum", "congue", "consectetuer", "consequat", "conubia", "convallis", "cras", "cubilia", "cum", "curabitur", "curae", "cursus", "dapibus", "diam", "dictum", "dictumst", "dignissim", "dis", "dolor", "donec", "dui", "duis", "egestas", "eget", "eleifend", "elementum", "elit", "enim", "erat", "eros", "est", "et", "etiam", "eu", "euismod", "facilisi", "facilisis", "fames", "faucibus", "felis", "fermentum", "feugiat", "fringilla", "fusce", "gravida", "habitant", "habitasse", "hac", "hendrerit", "hymenaeos", "iaculis", "id", "imperdiet", "in", "inceptos", "integer", "interdum", "ipsum", "justo", "lacinia", "lacus", "laoreet", "lectus", "leo", "libero", "ligula", "litora", "lobortis", "lorem", "luctus", "maecenas", "magna", "magnis", "malesuada", "massa", "mattis", "mauris", "metus", "mi", "molestie", "mollis", "montes", "morbi", "mus", "nam", "nascetur", "natoque", "nec", "neque", "netus", "nibh", "nisi", "nisl", "non", "nonummy", "nostra", "nulla", "nullam", "nunc", "odio", "orci", "ornare", "parturient", "pede", "pellentesque", "penatibus", "per", "pharetra", "phasellus", "placerat", "platea", "porta", "porttitor", "posuere", "potenti", "praesent", "pretium", "primis", "proin", "pulvinar", "purus", "quam", "quis", "quisque", "rhoncus", "ridiculus", "risus", "rutrum", "sagittis", "sapien", "scelerisque", "sed", "sem", "semper", "senectus", "sit", "sociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssoincidusociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "vsociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "sociosqu", "ssociis", "s", "vulputate", ]; let n_kwargs: Option = kwargs.get("n")?; let min: Option = kwargs.get("min")?; let min = min.unwrap_or(20); let max: Option = kwargs.get("max")?; let max = max.unwrap_or(100); let html: Option = kwargs.get("html")?; let html = html.unwrap_or(false); let n = n.or(n_kwargs).unwrap_or(5); let mut rv = String::new(); let rng = crate::rand::XorShiftRng::for_state(state); for _ in 0..n { let mut next_capitalized = true; let mut last_fullstop = 0; let mut last = ""; for idx in 0..rng.random_range(min as i64, max as i64) { if idx > 0 { rv.push(' '); } else if html { rv.push_str("

"); } let word = loop { let word = LIPSUM_WORDS[rng.next_usize(LIPSUM_WORDS.len())]; if word != last { last = word; break word; } }; if next_capitalized { for (idx, c) in word.char_indices() { if idx == 0 { use std::fmt::Write; write!(rv, "{}", c.to_uppercase()).ok(); } else { rv.push(c); } } next_capitalized = false; } else { rv.push_str(word); } if idx - last_fullstop > rng.random_range(10, 20) { rv.push('.'); last_fullstop = idx; next_capitalized = true; } } if !rv.ends_with('.') { rv.push('.'); } if html { rv.push_str("

"); } rv.push_str("\n\n"); } if html { Ok(Value::from_safe_string(rv)) } else { Ok(Value::from(rv)) } } python-minijinja-2.12.0/minijinja-contrib/src/lib.rs0000664000175000017500000000417415052560140022262 0ustar carstencarsten//! MiniJinja-Contrib is a utility crate for [MiniJinja](https://github.com/mitsuhiko/minijinja) //! that adds support for certain utilities that are too specific for the MiniJinja core. This is //! usually because they provide functionality that Jinja2 itself does not have. //! //! To add all of these to an environment you can use the [`add_to_environment`] function. //! //! ``` //! use minijinja::Environment; //! //! let mut env = Environment::new(); //! minijinja_contrib::add_to_environment(&mut env); //! ``` #![cfg_attr(docsrs, feature(doc_cfg))] use minijinja::Environment; /// Implements Python methods for better compatibility. #[cfg(feature = "pycompat")] pub mod pycompat; /// Utility filters. pub mod filters; /// Globals pub mod globals; #[cfg(feature = "html_entities")] mod html_entities; #[cfg(feature = "rand")] mod rand; /// Registers all features of this crate with an [`Environment`]. /// /// All the filters that are available will be added, same with global /// functions that exist. /// /// **Note:** the `pycompat` support is intentionally not registered /// with the environment. pub fn add_to_environment(env: &mut Environment) { env.add_filter("pluralize", filters::pluralize); env.add_filter("filesizeformat", filters::filesizeformat); env.add_filter("truncate", filters::truncate); env.add_filter("striptags", filters::striptags); #[cfg(feature = "wordcount")] { env.add_filter("wordcount", filters::wordcount); } #[cfg(feature = "wordwrap")] { env.add_filter("wordwrap", filters::wordwrap); } #[cfg(feature = "datetime")] { env.add_filter("datetimeformat", filters::datetimeformat); env.add_filter("timeformat", filters::timeformat); env.add_filter("dateformat", filters::dateformat); env.add_function("now", globals::now); } #[cfg(feature = "rand")] { env.add_filter("random", filters::random); env.add_function("lipsum", globals::lipsum); env.add_function("randrange", globals::randrange); } env.add_function("cycler", globals::cycler); env.add_function("joiner", globals::joiner); } python-minijinja-2.12.0/Makefile0000664000175000017500000000072515052560140016407 0ustar carstencarsten.PHONY: all all: develop test .venv: python3 -mvenv .venv .venv/bin/pip install --upgrade pip .venv/bin/pip install maturin pytest markupsafe black pyright .PHONY: test develop: .venv .venv/bin/maturin develop .PHONY: develop-release develop-release: .venv .venv/bin/maturin develop --release .PHONY: test test: .venv .venv/bin/pytest .PHONY: format format: .venv .venv/bin/black tests python .PHONY: type-check type-check: .venv .venv/bin/pyright python