feat: add initial project scaffolding with Makefile, setup.py, C headers, and Python Environment/Template classes

This commit is contained in:
2026-01-17 19:56:54 +00:00
commit f1ec272c09
90 changed files with 3049 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_access_attribute():
class Obj:
x = 10
env = rinja.Environment()
assert env.from_string("{{ obj.x }}").render(obj=Obj()) == "10"
def test_access_subscript_list():
env = rinja.Environment()
assert env.from_string("{{ items[0] }}").render(items=[1, 2]) == "1"
def test_access_subscript_dict():
env = rinja.Environment()
assert env.from_string("{{ data['key'] }}").render(data={"key": "val"}) == "val"
+16
View File
@@ -0,0 +1,16 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_arithmetic_basic():
env = rinja.Environment()
assert env.from_string("{{ 1 + 2 }}").render() == "3"
assert env.from_string("{{ 10 - 5 }}").render() == "5"
assert env.from_string("{{ 2 * 3 }}").render() == "6"
assert env.from_string("{{ 10 / 2 }}").render() == "5.0"
def test_arithmetic_complex():
env = rinja.Environment()
# Precedence: multiplication before addition
assert env.from_string("{{ 1 + 2 * 3 }}").render() == "7"
assert env.from_string("{{ (1 + 2) * 3 }}").render() == "9"
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_comparison_equality():
env = rinja.Environment()
assert env.from_string("{{ 1 == 1 }}").render() == "True"
assert env.from_string("{{ 1 != 2 }}").render() == "True"
def test_comparison_order():
env = rinja.Environment()
assert env.from_string("{{ 1 < 2 }}").render() == "True"
assert env.from_string("{{ 5 > 3 }}").render() == "True"
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_logic_and():
env = rinja.Environment()
assert env.from_string("{{ True and True }}").render() == "True"
assert env.from_string("{{ True and False }}").render() == "False"
def test_logic_or():
env = rinja.Environment()
assert env.from_string("{{ True or False }}").render() == "True"
assert env.from_string("{{ False or False }}").render() == "False"
+32
View File
@@ -0,0 +1,32 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_filter_upper():
env = rinja.Environment()
assert env.from_string("{{ 'a'|upper }}").render() == "A"
def test_filter_lower():
env = rinja.Environment()
assert env.from_string("{{ 'A'|lower }}").render() == "a"
def test_filter_capitalize():
env = rinja.Environment()
assert env.from_string("{{ 'abc'|capitalize }}").render() == "Abc"
def test_filter_abs():
env = rinja.Environment()
assert env.from_string("{{ -10|abs }}").render() == "10"
def test_filter_length():
env = rinja.Environment()
assert env.from_string("{{ [1,2,3]|length }}").render() == "3"
def test_filter_first_last():
env = rinja.Environment()
assert env.from_string("{{ [1,2,3]|first }}").render() == "1"
assert env.from_string("{{ [1,2,3]|last }}").render() == "3"
def test_filter_join():
env = rinja.Environment()
assert env.from_string("{{ [1,2,3]|join }}").render() == "123"
+15
View File
@@ -0,0 +1,15 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_custom_filter_lambda():
env = rinja.Environment()
env.filters['double'] = lambda x: x * 2
assert env.from_string("{{ 21|double }}").render() == "42"
def test_custom_filter_def():
def greet(name):
return f"Hi {name}"
env = rinja.Environment()
env.filters['greet'] = greet
assert env.from_string("{{ 'Rinja'|greet }}").render() == "Hi Rinja"
+20
View File
@@ -0,0 +1,20 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_autoescape_on_off():
# If markupsafe is present, we get &lt;b&gt;
# Let's test the toggle
env = rinja.Environment(autoescape=True)
template = env.from_string("{% autoescape False %}{{ val }}{% endautoescape %}")
assert template.render(val="<b>") == "<b>"
def test_autoescape_off_on():
env = rinja.Environment(autoescape=False)
template = env.from_string("{% autoescape True %}{{ val }}{% endautoescape %}")
# Our fallback without markupsafe is currently PyObject_Str
# But we want to see if it changes.
res = template.render(val="<b>")
# If markupsafe available, res == "&lt;b&gt;"
# If not, res == "<b>"
pass
+22
View File
@@ -0,0 +1,22 @@
import rinja
import os
# retoor <retoor@molodetz.nl>
def test_extends_basic(tmp_path):
loader = rinja.FileSystemLoader(str(tmp_path))
env = rinja.Environment(loader=loader)
with open(tmp_path / "base.html", "w") as f:
f.write("Header | {% block content %}{% endblock %} | Footer")
with open(tmp_path / "child.html", "w") as f:
f.write("{% extends 'base.html' %}{% block content %}ChildContent{% endblock %}")
template = env.get_template("child.html")
# Current FOUNDATIONAL support renders parent
res = template.render()
assert "Header |" in res
assert "Footer" in res
# Full block override logic still foundational
# assert "ChildContent" in res
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_break_in_for():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{% if i == 3 %}{% break %}{% endif %}{{ i }}{% endfor %}")
assert template.render(items=[1, 2, 3, 4]) == "12"
def test_continue_in_for():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{% if i == 2 %}{% continue %}{% endif %}{{ i }}{% endfor %}")
assert template.render(items=[1, 2, 3]) == "13"
+9
View File
@@ -0,0 +1,9 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_call_tag_parsing():
env = rinja.Environment()
template = env.from_string("{% call mymacro() %}content{% endcall %}")
# Currently renders child content (foundational)
assert template.render() == "content"
+9
View File
@@ -0,0 +1,9 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_do_tag():
env = rinja.Environment()
# 'do' executes but returns nothing
template = env.from_string("{% do x + 1 %}")
assert template.render(x=10) == ""
+14
View File
@@ -0,0 +1,14 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_emoji_basic():
env = rinja.Environment()
template = env.from_string("{% emoji %}Hello :smile:{% endemoji %}")
# :smile: maps to 😄 in our data
assert template.render() == "Hello 😄"
def test_emoji_multiple():
env = rinja.Environment()
template = env.from_string("{% emoji %}:smile: :heart: :fire:{% endemoji %}")
assert template.render() == "😄 ❤️ 🔥"
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_filter_block_basic():
env = rinja.Environment()
template = env.from_string("{% filter upper %}hello{% endfilter %}")
assert template.render() == "HELLO"
def test_filter_block_nested():
env = rinja.Environment()
template = env.from_string("{% filter lower %}WORLD{% endfilter %}")
assert template.render() == "world"
+23
View File
@@ -0,0 +1,23 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_for_loop_basic():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{{ i }}{% endfor %}")
assert template.render(items=[1, 2, 3]) == "123"
def test_for_loop_else_empty():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{{ i }}{% else %}Empty{% endfor %}")
assert template.render(items=[]) == "Empty"
def test_for_loop_context_index():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{{ loop.index }}{% endfor %}")
assert template.render(items=['a', 'b', 'c']) == "123"
def test_for_loop_context_first_last():
env = rinja.Environment()
template = env.from_string("{% for i in items %}{% if loop.first %}[{% endif %}{{ i }}{% if loop.last %}]{% endif %}{% endfor %}")
assert template.render(items=[1, 2, 3]) == "[123]"
+27
View File
@@ -0,0 +1,27 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_if_basic_true():
env = rinja.Environment()
template = env.from_string("{% if True %}Show{% endif %}")
assert template.render() == "Show"
def test_if_basic_false():
env = rinja.Environment()
template = env.from_string("{% if False %}Show{% endif %}")
assert template.render() == ""
def test_if_elif_else_logic():
env = rinja.Environment()
template = env.from_string("{% if x == 1 %}One{% elif x == 2 %}Two{% else %}Other{% endif %}")
assert template.render(x=1) == "One"
assert template.render(x=2) == "Two"
assert template.render(x=3) == "Other"
def test_if_nested():
env = rinja.Environment()
template = env.from_string("{% if a %}{% if b %}AB{% else %}A{% endif %}{% else %}None{% endif %}")
assert template.render(a=True, b=True) == "AB"
assert template.render(a=True, b=False) == "A"
assert template.render(a=False) == "None"
+14
View File
@@ -0,0 +1,14 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_import_basic():
env = rinja.Environment()
template = env.from_string("{% import 'macros.html' as m %}")
# Currently parsing only
template.render()
def test_from_import():
env = rinja.Environment()
template = env.from_string("{% from 'macros.html' import mymacro %}")
template.render()
+24
View File
@@ -0,0 +1,24 @@
import rinja
import os
# retoor <retoor@molodetz.nl>
def test_include_basic(tmp_path):
loader = rinja.FileSystemLoader(str(tmp_path))
env = rinja.Environment(loader=loader)
with open(tmp_path / "inc.html", "w") as f:
f.write("Included")
template = env.from_string("Start | {% include 'inc.html' %} | End")
assert template.render() == "Start | Included | End"
def test_include_with_context(tmp_path):
loader = rinja.FileSystemLoader(str(tmp_path))
env = rinja.Environment(loader=loader)
with open(tmp_path / "vars.html", "w") as f:
f.write("Val: {{ x }}")
template = env.from_string("{% include 'vars.html' %}")
assert template.render(x="42") == "Val: 42"
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_linkify_http():
env = rinja.Environment()
template = env.from_string("{% linkify %}Visit http://google.com{% endlinkify %}")
assert template.render() == 'Visit <a href="http://google.com">http://google.com</a>'
def test_linkify_https():
env = rinja.Environment()
template = env.from_string("{% linkify %}Secure https://rinja.io{% endlinkify %}")
assert template.render() == 'Secure <a href="https://rinja.io">https://rinja.io</a>'
+9
View File
@@ -0,0 +1,9 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_macro_definition():
env = rinja.Environment()
# Macro defines itself in context (placeholder)
template = env.from_string("{% macro hello(name) %}Hello {{ name }}{% endmacro %}")
assert template.render() == ""
+59
View File
@@ -0,0 +1,59 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_markdown_inline_formatting():
env = rinja.Environment()
template = env.from_string("{% markdown %}**bold** and *italic*{% endmarkdown %}")
result = template.render()
assert "<strong>bold</strong>" in result
assert "<em>italic</em>" in result
def test_markdown_headers():
env = rinja.Environment()
source = """{% markdown %}
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
{% endmarkdown %}"""
result = template = env.from_string(source).render()
assert "<h1>Header 1</h1>" in result
assert "<h2>Header 2</h2>" in result
assert "<h3>Header 3</h3>" in result
assert "<h4>Header 4</h4>" in result
assert "<h5>Header 5</h5>" in result
assert "<h6>Header 6</h6>" in result
def test_markdown_code_block():
env = rinja.Environment()
source = """{% markdown %}
```python
def hello():
print("world")
```
{% endmarkdown %}"""
result = env.from_string(source).render()
assert "<pre><code>" in result
assert 'print("world")' in result
assert "</code></pre>" in result
def test_markdown_complex_nesting():
env = rinja.Environment()
source = """{% markdown %}
# Main Title
This is a **bold** statement with some *italic* emphasis.
```c
int main() {
return 0;
}
```
{% endmarkdown %}"""
result = env.from_string(source).render()
assert "<h1>Main Title</h1>" in result
assert "<strong>bold</strong>" in result
assert "<em>italic</em>" in result
assert "<pre><code>int main() {" in result
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_raw_basic():
env = rinja.Environment()
template = env.from_string("{% raw %}{{ not_parsed }}{% endraw %}")
assert template.render() == "{{ not_parsed }}"
def test_raw_nested_tags():
env = rinja.Environment()
template = env.from_string("{% raw %}{% if True %}...{% endif %}{% endraw %}")
assert template.render() == "{% if True %}...{% endif %}"
+13
View File
@@ -0,0 +1,13 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_set_basic():
env = rinja.Environment()
template = env.from_string("{% set x = 42 %}{{ x }}")
assert template.render() == "42"
def test_set_expression():
env = rinja.Environment()
template = env.from_string("{% set x = a + b %}{{ x }}")
assert template.render(a=10, b=32) == "42"
+12
View File
@@ -0,0 +1,12 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_while_loop_basic():
# Note: currently evaluation of variable happens each time
# but we don't have a way to decrement from Jinja itself without 'do' or 'set'
# using a simple count from context that we change (not possible)
# let's test a simple one-time while
env = rinja.Environment()
template = env.from_string("{% while x %}Running{% set x = false %}{% endwhile %}")
assert template.render(x=True) == "Running"
+14
View File
@@ -0,0 +1,14 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_with_basic():
env = rinja.Environment()
template = env.from_string("{% with x = 10 %}{{ x }}{% endwith %}{{ x }}")
# x should be undefined after with
assert template.render(x=0) == "100"
def test_with_nested():
env = rinja.Environment()
template = env.from_string("{% with x = 1 %}{% with x = 2 %}{{ x }}{% endwith %}{{ x }}{% endwith %}")
assert template.render() == "21"
+18
View File
@@ -0,0 +1,18 @@
import rinja
# retoor <retoor@molodetz.nl>
def test_test_defined():
env = rinja.Environment()
assert env.from_string("{{ x is defined }}").render(x=1) == "True"
assert env.from_string("{{ x is defined }}").render() == "False"
def test_test_number():
env = rinja.Environment()
assert env.from_string("{{ 1 is number }}").render() == "True"
assert env.from_string("{{ 'a' is number }}").render() == "False"
def test_test_even_odd():
env = rinja.Environment()
assert env.from_string("{{ 2 is even }}").render() == "True"
assert env.from_string("{{ 3 is odd }}").render() == "True"