File size: 4,044 Bytes
ec94fc1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | """Utility functions shared across renderers.
Mirrors PHP helpers: h(), formatMoneyFigures(), handbook_anchor(), etc.
"""
from __future__ import annotations
import html
import re
def h(s: str) -> str:
"""HTML-escape (mirrors PHP h())."""
return html.escape(str(s), quote=True)
def is_assoc(a: list | dict) -> bool:
"""Check if an array is associative (dict-like) vs sequential list."""
return isinstance(a, dict)
def hb_slug(s: str) -> str:
"""Slug helper for anchors."""
tmp = s.lower().strip()
tmp = re.sub(r"[^a-z0-9]+", "_", tmp, flags=re.IGNORECASE)
tmp = re.sub(r"_+", "_", tmp)
return tmp.strip("_")
def handbook_anchor(prefix: str, text: str, idx: int) -> str:
"""Normalise a string into a safe anchor id. Mirrors PHP handbook_anchor."""
base = text.lower().strip()
base = re.sub(r"[^a-z0-9]+", "-", base, flags=re.IGNORECASE)
base = base.strip("-")
if not base:
base = f"{prefix}-{idx}"
return f"{prefix}-{base}-{idx}"
def is_truthy(val) -> bool:
"""Mirrors PHP handbook_true."""
if isinstance(val, bool):
return val
if isinstance(val, int):
return val != 0
v = str(val).lower().strip()
return v not in ("0", "false", "")
def format_money_figures(text: str) -> str:
"""Mirrors PHP formatMoneyFigures().
- Strips "USD " prefix
- Adds $ to large numbers
- Formats with commas
"""
if not text:
return text
# Remove "USD " prefix
text = re.sub(r"\bUSD\s*", "", text, flags=re.IGNORECASE)
def _format_match(m: re.Match) -> str:
num_str = m.group(1).replace(",", "")
dec = m.group(2) if m.group(2) else ""
num = float(num_str)
if dec:
formatted = f"{num:,.{len(dec)}f}"
else:
formatted = f"{num:,.0f}"
return "$" + formatted
# Add $ to large numbers (with optional decimals)
text = re.sub(
r"(?<![$0-9])(?<!\$)((?:\d{1,3}(?:,\d{3})+)|(?:\d{4,}))(?:\.(\d+))?(?![%\d/])",
_format_match,
text,
)
# Ensure remaining 4+ digit numbers are formatted with commas and $
def _format_remaining(m: re.Match) -> str:
num_str = m.group(1).replace(",", "")
formatted = f"{float(num_str):,.0f}"
return "$" + formatted
text = re.sub(
r"(?<!\$)\b(\d{1,3}(?:,\d{3})+|\d{4,})(?![%\d/])",
_format_remaining,
text,
)
return text
def sort_sections_stable(sections: list[dict]) -> list[dict]:
"""Stable sort: sort_order ASC, then id ASC, then insertion order."""
for i, s in enumerate(sections):
s.setdefault("_i", i)
def sort_key(s: dict):
so = s.get("sort_order")
sid = s.get("id")
so_key = (0, so) if so is not None else (1, 0)
sid_key = (0, sid) if sid is not None else (1, 0)
return (so_key, sid_key, s.get("_i", 0))
sections.sort(key=sort_key)
for s in sections:
s.pop("_i", None)
return sections
def get_any(d: dict, keys: list[str]) -> str:
"""Return the first non-empty string value found for one of the keys."""
for k in keys:
v = d.get(k)
if v is None or isinstance(v, (dict, list)):
continue
t = str(v).strip()
if t:
return t
return ""
def emphasize_keywords(text: str) -> str:
"""Add bold HTML emphasis to key handbook terms in already-escaped text.
Bolds: REGULAR, PRIME, dollar amounts ($X,XXX), and other critical terms.
Input must already be HTML-escaped. Returns HTML with <strong> tags.
"""
if not text:
return text
escaped = h(text)
# Bold REGULAR and PRIME (case-insensitive, whole word)
escaped = re.sub(
r'\b(REGULAR|PRIME)\b',
r'<strong>\1</strong>',
escaped,
flags=re.IGNORECASE,
)
# Bold dollar amounts like $1,000 or $500
escaped = re.sub(
r'(\$[\d,]+(?:\.\d+)?)',
r'<strong>\1</strong>',
escaped,
)
return escaped
|