"""Provides standard liquid filters"""
import re
import math
import html
from datetime import datetime
from jinja2.filters import FILTERS
from .manager import FilterManager
standard_filter_manager = FilterManager()
class DateTime:DOCS
"""Date time allows plus/minus operation"""
def __init__(self, dt: datetime, fmt: str) -> None:
self.dt = dt
self.fmt = fmt
def __str__(self) -> str:DOCS
"""How it is rendered"""
return self.dt.strftime(self.fmt)
def __add__(self, other: int) -> int:
return int(str(self)) + other
def __sub__(self, other: int) -> int:
return int(str(self)) - other
def __mul__(self, other: int) -> int:
return int(str(self)) * other
def __floordiv__(self, other: int) -> float:
return float(str(self)) // other
def __mod__(self, other: int) -> int:
return int(str(self)) % other
def __pow__(self, other: int) -> int: # pragma: no cover
return int(str(self)) ** other
def __truediv__(self, other: int) -> float: # pragma: no cover
return float(str(self)) / other
def __radd__(self, other: int) -> int: # pragma: no cover
return other + int(str(self))
def __rsub__(self, other: int) -> int: # pragma: no cover
return other - int(str(self))
def __rmul__(self, other: int) -> int: # pragma: no cover
return other * int(str(self))
def __rmod__(self, other: int) -> int: # pragma: no cover
return other % int(str(self))
def __rpow__(self, other: int) -> int: # pragma: no cover
return other ** int(str(self))
def __rtruediv__(self, other: int) -> float: # pragma: no cover
return other / float(str(self))
def __rfloordiv__(self, other: int) -> float: # pragma: no cover
return other // float(str(self))
class EmptyDrop:DOCS
"""The EmptyDrop class borrowed from liquid"""
# Use jinja's Undefined instead?
def __init__(self):
setattr(self, "empty?", True)
def __str__(self):
return ""
def __eq__(self, other):
return not bool(other)
def __ne__(self, other):
return not self.__eq__(other)
def __bool__(self):
return False
def _get_prop(obj, prop, _raise=False):
"""Get the property of the object, allow via getitem"""
try:
return obj[prop]
except (TypeError, KeyError):
try:
return getattr(obj, prop)
except AttributeError:
if _raise: # pragma: no cover
raise
return None
# Jinja comes with thses filters
# standard_filter_manager.register(str.capitalize)
# standard_filter_manager.register(abs)
# standard_filter_manager.register(round)
standard_filter_manager.register("concat")(list.__add__)
standard_filter_manager.register("at_least")(max)
standard_filter_manager.register("at_most")(min)
standard_filter_manager.register("downcase")(str.lower)
standard_filter_manager.register("upcase")(str.upper)
standard_filter_manager.register(html.escape)
standard_filter_manager.register(str.lstrip)
standard_filter_manager.register(str.rstrip)
standard_filter_manager.register(str.strip)
standard_filter_manager.register(str.replace)
standard_filter_manager.register("size")(len)
standard_filter_manager.register(int)
standard_filter_manager.register(float)
standard_filter_manager.register(str)
standard_filter_manager.register(bool)
@standard_filter_manager.registerDOCS
def split(base, sep):
"""Split a string into a list
If the sep is empty, return the list of characters
"""
if not sep:
return list(base)
return base.split(sep)
@standard_filter_manager.registerDOCS
def append(base, suffix):
"""Append a suffix to a string"""
return f"{base}{suffix}"
@standard_filter_manager.registerDOCS
def prepend(base, prefix):
"""Prepend a prefix to a string"""
return f"{prefix}{base}"
@standard_filter_manager.registerDOCS
def times(base, sep):
"""Implementation of *"""
return base * sep
@standard_filter_manager.registerDOCS
def minus(base, sep):
"""Implementation of -"""
return base - sep
@standard_filter_manager.registerDOCS
def plus(base, sep):
"""Implementation of +"""
return base + sep
@standard_filter_manager.registerDOCS
def modulo(base, sep):
"""Implementation of %"""
return base % sep
@standard_filter_manager.registerDOCS
def ceil(base):
"""Get the ceil of a number"""
return math.ceil(float(base))
@standard_filter_manager.registerDOCS
def floor(base):
"""Get the floor of a number"""
return math.floor(float(base))
@standard_filter_manager.register("date")DOCS
def liquid_date(base, fmt):
"""Format a date/datetime"""
if base == "now":
dtime = datetime.now()
elif base == "today":
dtime = datetime.today()
elif isinstance(base, (int, float)):
dtime = datetime.fromtimestamp(base)
else:
from dateutil import parser # type: ignore
dtime = parser.parse(base)
return DateTime(dtime, fmt)
@standard_filter_manager.registerDOCS
def default(base, deft, allow_false=False):
"""Return the deft value if base is not set.
Otherwise, return base"""
if allow_false and base is False:
return False
if base is None:
return deft
return FILTERS["default"](base, deft, isinstance(base, str))
@standard_filter_manager.registerDOCS
def divided_by(base, dvdby):
"""Implementation of / or //"""
if isinstance(dvdby, int):
return base // dvdby
return base / dvdby
@standard_filter_manager.registerDOCS
def escape_once(base):
"""Escapse html characters only once of the string"""
return html.escape(html.unescape(base))
@standard_filter_manager.registerDOCS
def newline_to_br(base):
"""Replace newline with `<br />`"""
return base.replace("\n", "<br />")
@standard_filter_manager.registerDOCS
def remove(base, string):
"""Remove a substring from a string"""
return base.replace(string, "")
@standard_filter_manager.registerDOCS
def remove_first(base, string):
"""Remove the first substring from a string"""
return base.replace(string, "", 1)
@standard_filter_manager.registerDOCS
def replace_first(base, old, new):
"""Replace the first substring with new string"""
return base.replace(old, new, 1)
# @standard_filter_manager.register
# def reverse(base):
# """Get the reversed list"""
# if not base:
# return EmptyDrop()
# return list(reversed(base))
@standard_filter_manager.registerDOCS
def sort(base):
"""Get the sorted list"""
if not base:
return EmptyDrop()
return list(sorted(base))
@standard_filter_manager.registerDOCS
def sort_natural(base):
"""Get the sorted list in a case-insensitive manner"""
if not base:
return EmptyDrop()
return list(sorted(base, key=str.casefold))
@standard_filter_manager.register("slice")DOCS
def liquid_slice(base, start, length=1):
"""Slice a list"""
if not base:
return EmptyDrop()
if start < 0:
start = len(base) + start
end = None if length is None else start + length
return base[start:end]
@standard_filter_manager.registerDOCS
def strip_html(base):
"""Strip html tags from a string"""
# use html parser?
return re.sub(r"<[^>]+>", "", base)
@standard_filter_manager.registerDOCS
def strip_newlines(base):
"""Strip newlines from a string"""
return base.replace("\n", "")
@standard_filter_manager.registerDOCS
def truncate(base, length, ellipsis="..."):
"""Truncate a string"""
lenbase = len(base)
if length >= lenbase:
return base
return base[: length - len(ellipsis)] + ellipsis
@standard_filter_manager.registerDOCS
def truncatewords(base, length, ellipsis="..."):
"""Truncate a string by words"""
# do we need to preserve the whitespaces?
baselist = base.split()
lenbase = len(baselist)
if length >= lenbase:
return base
# instead of collapsing them into just a single space?
return " ".join(baselist[:length]) + ellipsis
@standard_filter_manager.registerDOCS
def uniq(base):
"""Get the unique elements from a list"""
if not base:
return EmptyDrop()
ret = []
for bas in base:
if bas not in ret:
ret.append(bas)
return ret
@standard_filter_manager.registerDOCS
def url_decode(base):
"""Url-decode a string"""
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
return unquote(base)
@standard_filter_manager.registerDOCS
def url_encode(base):
"""Url-encode a string"""
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
return urlencode({"": base})[1:]
@standard_filter_manager.registerDOCS
def where(base, prop, value):
"""Query a list of objects with a given property value"""
ret = [bas for bas in base if _get_prop(bas, prop) == value]
return ret or EmptyDrop()
@standard_filter_manager.register(["liquid_map", "map"])DOCS
def liquid_map(base, prop):
"""Map a property to a list of objects"""
return [_get_prop(bas, prop) for bas in base]
@standard_filter_manager.registerDOCS
def attr(base, prop):
"""Similar as `__getattr__()` but also works like `__getitem__()"""
return _get_prop(base, prop)
# @standard_filter_manager.register
# def join(base, sep):
# """Join a list by the sep"""
# if isinstance(base, EmptyDrop):
# return ''
# return sep.join(base)
# @standard_filter_manager.register
# def first(base):
# """Get the first element of the list"""
# if not base:
# return EmptyDrop()
# return base[0]
# @standard_filter_manager.register
# def last(base):
# """Get the last element of the list"""
# if not base:
# return EmptyDrop()
# return base[-1]
@standard_filter_manager.registerDOCS
def compact(base):
"""Remove empties from a list"""
ret = [bas for bas in base if bas]
return ret or EmptyDrop()
@standard_filter_manager.registerDOCS
def regex_replace(
base: str,
regex: str,
replace: str = "",
case_sensitive: bool = False,
count: int = 0,
) -> str:
"""Replace matching regex pattern"""
if not isinstance(base, str):
# Raise an error instead?
return base
args = {
"pattern": regex, # re.escape
"repl": replace,
"string": base,
"count": count,
}
if not case_sensitive:
args["flags"] = re.IGNORECASE
return re.sub(**args) # type: ignore