CoolFace
Apppublic

internationalscholarsprogram/handbook-engine

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
utils.py260 linesDownload Raw Back to services
1"""Utility functions shared across renderers.2 3Mirrors PHP helpers: h(), formatMoneyFigures(), handbook_anchor(), etc.4"""5 6from __future__ import annotations7 8import html9import re10 11 12def h(s: str) -> str:13    """HTML-escape (mirrors PHP h())."""14    return html.escape(str(s), quote=True)15 16 17def is_assoc(a: list | dict) -> bool:18    """Check if an array is associative (dict-like) vs sequential list."""19    return isinstance(a, dict)20 21 22def hb_slug(s: str) -> str:23    """Slug helper for anchors."""24    tmp = s.lower().strip()25    tmp = re.sub(r"[^a-z0-9]+", "_", tmp, flags=re.IGNORECASE)26    tmp = re.sub(r"_+", "_", tmp)27    return tmp.strip("_")28 29 30def handbook_anchor(prefix: str, text: str, idx: int) -> str:31    """Normalise a string into a safe anchor id. Mirrors PHP handbook_anchor."""32    base = text.lower().strip()33    base = re.sub(r"[^a-z0-9]+", "-", base, flags=re.IGNORECASE)34    base = base.strip("-")35    if not base:36        base = f"{prefix}-{idx}"37    return f"{prefix}-{base}-{idx}"38 39 40def is_truthy(val) -> bool:41    """Mirrors PHP handbook_true."""42    if isinstance(val, bool):43        return val44    if isinstance(val, int):45        return val != 046    v = str(val).lower().strip()47    return v not in ("0", "false", "")48 49 50def format_money_figures(text: str) -> str:51    """Normalize all monetary figures to "USD X,XXX" format.52 53    - Converts existing $X,XXX → USD X,XXX54    - Normalizes bare large numbers (1,000+) → USD X,XXX55    - Formats with commas56    - Currency type is always USD (no $ symbol)57    """58    if not text:59        return text60 61    # Step 1: Convert "$X" → "USD X" directly (preserves ALL dollar amounts)62    def _dollar_to_usd(m: re.Match) -> str:63        num_str = m.group(1).replace(",", "")64        try:65            num = float(num_str)66        except ValueError:67            return m.group(0)68        if "." in m.group(1):69            dec_part = m.group(1).split(".")[-1]70            formatted = f"{num:,.{len(dec_part)}f}"71        elif num == int(num):72            formatted = f"{int(num):,}"73        else:74            formatted = f"{num:,.2f}"75        return "USD " + formatted76 77    text = re.sub(r'\$([\d,]+(?:\.\d+)?)', _dollar_to_usd, text)78 79    # Step 2: Normalize existing "USD X,XXX" for consistent comma formatting80    def _normalize_usd(m: re.Match) -> str:81        num_str = m.group(1).replace(",", "")82        try:83            num = float(num_str)84        except ValueError:85            return m.group(0)86        if "." in m.group(1):87            dec_part = m.group(1).split(".")[-1]88            formatted = f"{num:,.{len(dec_part)}f}"89        elif num == int(num):90            formatted = f"{int(num):,}"91        else:92            formatted = f"{num:,.2f}"93        return "USD " + formatted94 95    text = re.sub(r'\bUSD\s+([\d,]+(?:\.\d+)?)', _normalize_usd, text, flags=re.IGNORECASE)96 97    # Step 3: Add "USD " to bare large numbers (4+ digits or comma-formatted)98    # that aren't already preceded by "USD "99    def _format_bare_large(m: re.Match) -> str:100        num_str = m.group(1).replace(",", "")101        dec = m.group(2) if m.group(2) else ""102        try:103            num = float(num_str)104        except ValueError:105            return m.group(0)106        if dec:107            formatted = f"{num:,.{len(dec)}f}"108        else:109            formatted = f"{num:,.0f}"110        return "USD " + formatted111 112    text = re.sub(113        r"(?<!\d)(?<!USD )((?:\d{1,3}(?:,\d{3})+)|(?:\d{4,}))(?:\.(\d+))?(?![%\d/])",114        _format_bare_large,115        text,116    )117 118    return text119 120 121def ensure_program_options_pair(text: str) -> str:122    """Ensure REGULAR/PRIME program options appear together when either appears.123 124    If only one of the two appears in text, append "(REGULAR and PRIME)"125    to preserve source meaning while enforcing consistency.126    """127    if not text:128        return text129 130    has_regular = bool(re.search(r"\bREGULAR\b", text, flags=re.IGNORECASE))131    has_prime = bool(re.search(r"\bPRIME\b", text, flags=re.IGNORECASE))132 133    if has_regular ^ has_prime:134        if re.search(r"\(\s*REGULAR\s+and\s+PRIME\s*\)", text, flags=re.IGNORECASE):135            return text136        return text.rstrip() + " (REGULAR and PRIME)"137 138    return text139 140 141def sort_sections_stable(sections: list[dict]) -> list[dict]:142    """Stable sort: sort_order ASC, then id ASC, then insertion order."""143    for i, s in enumerate(sections):144        s.setdefault("_i", i)145 146    def sort_key(s: dict):147        so = s.get("sort_order")148        sid = s.get("id")149        so_key = (0, so) if so is not None else (1, 0)150        sid_key = (0, sid) if sid is not None else (1, 0)151        return (so_key, sid_key, s.get("_i", 0))152 153    sections.sort(key=sort_key)154    for s in sections:155        s.pop("_i", None)156    return sections157 158 159def get_any(d: dict, keys: list[str]) -> str:160    """Return the first non-empty string value found for one of the keys."""161    for k in keys:162        v = d.get(k)163        if v is None or isinstance(v, (dict, list)):164            continue165        t = str(v).strip()166        if t:167            return t168    return ""169 170 171def emphasize_keywords(text: str) -> str:172    """Add bold HTML emphasis to key handbook terms in already-escaped text.173 174    Bolds: REGULAR, PRIME, dollar amounts ($X,XXX), and other critical terms.175    Input must already be HTML-escaped. Returns HTML with <strong> tags.176    """177    if not text:178        return text179 180    escaped = h(text)181 182    # Bold REGULAR and PRIME (case-insensitive, whole word)183    escaped = re.sub(184        r'\b(REGULAR|PRIME)\b',185        r'<strong>\1</strong>',186        escaped,187        flags=re.IGNORECASE,188    )189 190    # Bold USD amounts like USD 1,000 or USD 500191    escaped = re.sub(192        r'\b(USD\s+[\d,]+(?:\.\d+)?)',193        r'<strong>\1</strong>',194        escaped,195        flags=re.IGNORECASE,196    )197 198    # Bold standalone USD199    escaped = re.sub(200        r'\b(USD)\b(?!\s*[\d,])',201        r'<strong>\1</strong>',202        escaped,203        flags=re.IGNORECASE,204    )205 206    # Bold dollar-sign amounts like $20, $1,000, $1,000.00207    escaped = re.sub(208        r'(\$[\d,]+(?:\.\d+)?)',209        r'<strong>\1</strong>',210        escaped,211    )212 213    # Bold specific GPA values 2.8, 3.4 and 4.0214    escaped = re.sub(215        r'\b(2\.8|3\.4|4\.0)\b',216        r'<strong>\1</strong>',217        escaped,218    )219 220    # Bold key qualification and geo terms.221    escaped = re.sub(222        r'\b(GPA\s*\(\s*Undergraduate\s+Requirement\s*\)|GPA|High\s+School\s+grades|Global|Uganda|Kenya)\b',223        r'<strong>\1</strong>',224        escaped,225        flags=re.IGNORECASE,226    )227 228    # Bold refund policy phrase.229    escaped = re.sub(230        r'\b(Refund\s+Policy)\b',231        r'<strong>\1</strong>',232        escaped,233        flags=re.IGNORECASE,234    )235 236    return escaped237 238 239def linkify_urls(text: str) -> str:240    """Convert URLs in text to clickable <a> tags with target="_blank".241    242    Detects http/https URLs and converts them to proper anchor tags.243    Input should be plain text or already HTML-escaped.244    Returns HTML with <a> tags.245    """246    if not text:247        return text248    249    # Detect and convert http/https URLs to clickable links250    # Pattern: http:// or https:// followed by domain and optional path251    url_pattern = r'(https?://[^\s<)]+)'252    253    def make_link(match):254        url = match.group(1)255        # Clean up trailing punctuation that's likely not part of URL256        url = url.rstrip('.,;:!?)\'\"')257        return f'<a href="{h(url)}" target="_blank" rel="noopener noreferrer">{h(url)}</a>'258    259    return re.sub(url_pattern, make_link, text)260