|
# retoor <retoor@molodetz.nl>
|
|
|
|
import html
|
|
import json
|
|
import re
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
|
|
|
|
def strip_html(text: str) -> str:
|
|
if not text:
|
|
return ""
|
|
text = re.sub(r"<[^>]+>", " ", text)
|
|
text = html.unescape(text)
|
|
return re.sub(r"\s+", " ", text).strip()
|
|
|
|
|
|
_FLOAT_TOKEN = "@dpfloat@"
|
|
|
|
|
|
def plain_float(value: float) -> str:
|
|
if value != value or value in (float("inf"), float("-inf")):
|
|
return json.dumps(value)
|
|
return format(Decimal(repr(value)), "f")
|
|
|
|
|
|
def _mark_floats(node):
|
|
if isinstance(node, bool):
|
|
return node
|
|
if isinstance(node, float):
|
|
return f"{_FLOAT_TOKEN}{plain_float(node)}{_FLOAT_TOKEN}"
|
|
if isinstance(node, dict):
|
|
return {key: _mark_floats(value) for key, value in node.items()}
|
|
if isinstance(node, (list, tuple)):
|
|
return [_mark_floats(item) for item in node]
|
|
return node
|
|
|
|
|
|
def pretty_json(data, indent: int = 2) -> str:
|
|
text = json.dumps(
|
|
_mark_floats(data), indent=indent, ensure_ascii=False, default=str
|
|
)
|
|
return text.replace(f'"{_FLOAT_TOKEN}', "").replace(f'{_FLOAT_TOKEN}"', "")
|
|
|
|
|
|
def slugify(text: str) -> str:
|
|
text = text.lower().strip()
|
|
text = re.sub(r"[^a-z0-9-]", "-", text)
|
|
text = re.sub(r"-+", "-", text)
|
|
return text.strip("-")
|
|
|
|
|
|
def generate_uid() -> str:
|
|
import uuid_utils
|
|
|
|
return str(uuid_utils.uuid7())
|
|
|
|
|
|
def make_combined_slug(text: str, uid: str) -> str:
|
|
short_uid = uid.replace("-", "")[-12:]
|
|
slug_part = slugify(text)
|
|
if not slug_part:
|
|
return short_uid
|
|
return f"{short_uid}-{slug_part}"
|
|
|
|
|
|
def time_ago(dt_str: str) -> str:
|
|
dt = datetime.fromisoformat(dt_str)
|
|
if dt.tzinfo is None:
|
|
dt = dt.replace(tzinfo=timezone.utc)
|
|
now = datetime.now(timezone.utc)
|
|
diff = now - dt
|
|
days = diff.days
|
|
if days > 30:
|
|
return dt.strftime("%d/%m/%Y")
|
|
if days > 0:
|
|
return f"{days}d ago"
|
|
hours = diff.seconds // 3600
|
|
if hours > 0:
|
|
return f"{hours}h ago"
|
|
minutes = diff.seconds // 60
|
|
if minutes > 0:
|
|
return f"{minutes}m ago"
|
|
return "just now"
|
|
|
|
|
|
def extract_mentions(content: str) -> list[str]:
|
|
if not content:
|
|
return []
|
|
return re.findall(r"(?:^|[\s(])@([a-zA-Z0-9_-]+)", content)
|
|
|
|
|
|
def format_date(dt_str: str, include_time: bool = False) -> str:
|
|
if not dt_str:
|
|
return ""
|
|
try:
|
|
dt = datetime.fromisoformat(dt_str)
|
|
if include_time:
|
|
return dt.strftime("%d/%m/%Y %H:%M")
|
|
return dt.strftime("%d/%m/%Y")
|
|
except (ValueError, TypeError):
|
|
return dt_str
|