#!/usr/bin/env python3 """Report readable length diagnostics for LaTeX resume bullets. This helper has no target bands and no page-fill logic. Rendered character and word counts are observations; they do not determine whether a bullet is good. Usage: python resume_builder/helpers/char_count.py "\\item Built ..." python resume_builder/helpers/char_count.py output/Role/resume.tex python resume_builder/helpers/char_count.py --raw "bullet text" """ from __future__ import annotations import argparse import re import sys from pathlib import Path def strip_latex(text: str) -> str: """Approximate reader-visible text without imposing layout targets.""" text = re.sub(r"\\item\s*(\[\s*\])?\s*", "", text) text = re.sub(r"\\href\{[^}]*\}\{([^}]*)\}", r"\1", text) for command in ("textbf", "textit", "underline", "emph", "ce"): text = re.sub(rf"\\{command}\{{([^}}]*)\}}", r"\1", text) text = re.sub(r"\$\^\{?\\circ\}?\$", "°", text) text = re.sub(r"\$\^\{([^}]*)\}\$", r"\1", text) text = re.sub(r"\$\^(.)\$", r"\1", text) text = text.replace("$\\sim$", "~").replace("\\sim", "~") text = text.replace("---", "—").replace("--", "–") text = text.replace("\\&", "&").replace("\\%", "%") text = re.sub(r"\\[A-Za-z]+\s*", "", text) text = text.replace("$", "").replace("{", "").replace("}", "") return re.sub(r"\s+", " ", text).strip() def extract_items(source: str) -> list[str]: """Extract item bodies, including wrapped source lines.""" items: list[str] = [] current: list[str] = [] for line in source.splitlines(): stripped = line.strip() if stripped.startswith("\\item"): if current: items.append(" ".join(current)) current = [stripped] elif current and not stripped.startswith("\\end{itemize}"): current.append(stripped) elif current: items.append(" ".join(current)) current = [] if current: items.append(" ".join(current)) return items def diagnose(raw: str) -> tuple[str, int, int, int]: rendered = strip_latex(raw) words = re.findall(r"\b[\w+#./-]+\b", rendered, flags=re.UNICODE) clauses = len(re.findall(r"[;:]|\s—\s", rendered)) + 1 if rendered else 0 return rendered, len(rendered), len(words), clauses def format_report(raw: str) -> str: rendered, characters, words, clauses = diagnose(raw) note = "" if words > 35 or clauses > 3: note = "\n Review: potentially dense; check whether it contains multiple accomplishments." return ( f" {words} words | {characters} rendered characters | {clauses} clause(s)\n" f" Rendered: {rendered}{note}" ) def main() -> None: parser = argparse.ArgumentParser(description="Report LaTeX bullet length diagnostics") parser.add_argument("input", nargs="?", help="Bullet text or .tex file") parser.add_argument("--raw", action="store_true", help="Output rendered character count only") args = parser.parse_args() if args.input and args.input.endswith(".tex") and Path(args.input).is_file(): items = extract_items(Path(args.input).read_text(encoding="utf-8")) if not items: print("No \\item bullets found.") return for index, item in enumerate(items, 1): if args.raw: print(diagnose(item)[1]) else: print(f"Bullet {index}:\n{format_report(item)}\n") return raw = args.input if args.input is not None else sys.stdin.read().strip() if not raw: parser.error("provide bullet text, a .tex file, or stdin") print(diagnose(raw)[1] if args.raw else format_report(raw)) if __name__ == "__main__": main()