181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
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)
|