102 lines
3.7 KiB
Python
102 lines
3.7 KiB
Python
#!/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()
|