internationalscholarsprogram's picture
fix: ISP handbook styling overhaul - margins, typography, emphasis, benefits, CSS cascade
ec94fc1
"""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