Files
claude-resume-kit/resume_builder/helpers/char_count.py
T

102 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()