Initial commit
commit
3c0beb7ff6
|
@ -0,0 +1,10 @@
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
|
@ -0,0 +1 @@
|
||||||
|
3.11
|
|
@ -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.
|
|
@ -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, you’ll find some usage examples—since I don’t 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**.
|
|
@ -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)
|
|
@ -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 = []
|
Loading…
Reference in New Issue