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