Initial commit

main
kirbylife 2025-04-18 21:47:23 -06:00
commit 3c0beb7ff6
7 changed files with 230 additions and 0 deletions

10
.gitignore vendored 100644
View File

@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

1
.python-version 100644
View File

@ -0,0 +1 @@
3.11

13
LICENSE.md 100644
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2025 kirbylife <hola@kirbylife.dev>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

12
README.md 100644
View File

@ -0,0 +1,12 @@
# Jinja Low-Cost
This is a toy template engine built using only what the Python standard library includes.
It was created for three reasons:
- Because at one of my jobs, I needed a dependency-free template engine.
- As an experiment to see how far I could extend Python's default `Formatter`.
- To write an article for [my blog](https://blog.kirbylife.dev/post/creando-un-motor-de-plantillas-ligero-y-malo-en-python-10).
Inside `jinja_lowcost/main`, at the bottom of the file, youll find some usage examples—since I dont plan to document it properly.
This code is released under the **WTFPL 2.0** license, so feel free to use it however you like. But please, **do not use it in production**.

View File

@ -0,0 +1,180 @@
from collections.abc import Callable
from string import Formatter
from typing import Any, Iterable
from enum import Enum
import re
import shlex
class Instruction(Enum):
REPEAT = "repeat"
JOIN = "join"
IF = "if"
LOWER = "lower"
UPPER = "upper"
STRIP = "strip"
CALL = "call"
NOP = "nop"
class ConditionalOp(Enum):
NOT = "not"
EQUAL = "equal"
NOTEQUAL = "notequal"
EVEN = "even"
ODD = "odd"
CONTAINS = "contains"
INSTRUCTION_PATTERN = re.compile("""(?!['|"].*):(?!.*['|"])""")
class JinjaLowCost(Formatter):
@staticmethod
def parse_instruction(raw_instruction: str) -> tuple[Instruction, list[Any], str | None]:
# Separar la instrucción con sus parámetros y la template
binding = iter(re.split(INSTRUCTION_PATTERN, raw_instruction, maxsplit=1))
pre_instruction = next(binding, None)
# Sí no tiene ninguna instrucción, se retorna NOP
if not pre_instruction:
return Instruction.NOP, [], ""
template = next(binding, None)
# Separar la instrucción de sus parámetros respetando las comillas
instruction, *params = shlex.split(pre_instruction)
try:
# Retornar todas las partes sí la instrucción está en el Enum
return Instruction(instruction), params, template
except ValueError:
# Si no, retornar NOP
return Instruction.NOP, [], ""
def format_field(self, value: Any, format_spec: str) -> Any:
instruction, params, template = self.parse_instruction(format_spec)
match instruction:
case Instruction.REPEAT:
if isinstance(value, dict):
elements = value.items()
elif isinstance(value, int):
elements = range(value)
elif isinstance(value, Iterable):
elements = value
else:
raise Exception(f"'{value}' is not iterable")
if template is None:
raise Exception("A template is needed")
name = next(iter(params), "item")
processed_elements = []
for i, item in enumerate(elements):
processed_element = self.format(template, **{name: item, "#": i})
processed_elements.append(processed_element)
return "".join(processed_elements)
case Instruction.IF:
if not params:
return template if value else ""
iter_params = iter(params)
try:
conditional = ConditionalOp(next(iter_params))
except ValueError:
raise Exception(f"'{params[0]}' is not a valid if conditional")
criteria = next(iter_params, None)
match conditional:
case ConditionalOp.EQUAL:
if criteria is None:
raise Exception("Equal operator requires a parameter")
func = lambda: value == type(value)(criteria)
case ConditionalOp.NOTEQUAL:
if criteria is None:
raise Exception("NotEqual operator requires a parameter")
func = lambda: value != type(value)(criteria)
case ConditionalOp.CONTAINS:
if criteria is None:
raise Exception("Contains operator requires a parameter")
func = lambda: value and type(value[0])(criteria) in value
case ConditionalOp.EVEN:
func = lambda: value % 2 == 0
case ConditionalOp.ODD:
func = lambda: value % 2 != 0
case ConditionalOp.NOT:
func = lambda: not value
return template if func() else ""
case Instruction.JOIN:
if not isinstance(value, Iterable):
raise Exception(f"{value} is not iterable")
criteria = next(iter(params), "")
return criteria.join(map(str, value))
case Instruction.UPPER:
return str(value).upper()
case Instruction.LOWER:
return str(value).lower()
case Instruction.STRIP:
param = next(iter(params), None)
return str(value).strip(param)
case Instruction.CALL:
if not isinstance(value, Callable):
raise Exception(f"{value} is not callable")
return str(value(*params))
case Instruction.NOP:
try:
return super().format_field(value, format_spec)
except:
raise Exception(f"Error trying to format '{format_spec}' instruction")
f = JinjaLowCost()
res = f.format("{x:join ', '}", x=[1, 2, 3, 4])
print(res)
res = f.format("{x:strip '-'}", x="----prueba----")
print(res)
# res = f.format("{a=}", a=10)
# print(res)
res = f.format("{x:repeat n:->{{n}}<-\n}", x=[10, 20, 30])
print(res)
res = f.format("{x:if contains {y}:estoy dentro}", x=[10, 20, 30], y=10)
print(res)
res = f.format("{x:if contains 42:42 is real}", x=range(10, 50))
print(res)
res = f.format("{x:join ', '}", x=[10, 20, 30])
print(res)
res = f.format("{x:upper}", x="Kirbylife")
print(res)
res = f.format("{x:lower}", x="TESTtestTEST")
print(res)
res = f.format("{x.get:call key_a}", x={"key_a": 10, "key_b": 20})
print(res)
res = f.format("{names:repeat name:{{#:if even:+ {{name}}\n}}{{#:if odd:- {{name:upper}}\n}}}", names=["Pastor", "Suadero", "Tripa"])
print(res)
template = "{numbers:repeat num:{{num}} }"
print(f.format(template, numbers=[10, 20, 30]))
res = f.format("{tacos: repeat taco:{{#}}. {{taco:upper}}\n}", tacos=["suadero", "pasTor", "tripa"])
print(res)
res = f.format(
"{nums:repeat num:{{num: repeat n:{{{{n:02}}}} }}\n}",
nums=[range(3), range(3,6), range(6, 9), range(9, 12)]
)
print(res)
res = f.format("{n:repeat:{{#:repeat:* }}\n}", n=7)
print(res)
template = """
<ul>{cities:repeat city:
<li style="background-color: {{#:if even:gray}}{{#:if odd:lightgray}}">{{city}}</li>}
</ul>
"""
print(f.format(template, cities=["New York", "New Delhi", "Tokio", "Monterrey"]))
res = f.format("{name.title:call}", name="JiNjA LoW cOsT")
print(res)
res = f.format("{exchange.get:call {currency}} {currency}", exchange={"USD": 1, "MXN": 19.7}, currency="MXN")
print(res)

7
pyproject.toml 100644
View File

@ -0,0 +1,7 @@
[project]
name = "jinja-lowcost"
version = "0.1.0"
description = "proof-of-concept of a simple template engine written from scratch*"
readme = "README.md"
requires-python = ">=3.10"
dependencies = []

7
uv.lock 100644
View File

@ -0,0 +1,7 @@
version = 1
requires-python = ">=3.10"
[[package]]
name = "jinja-lowcost"
version = "0.1.0"
source = { virtual = "." }