diff --git a/tools/rewrap-comments.py b/tools/rewrap-comments.py new file mode 100755 index 0000000..813bc2e --- /dev/null +++ b/tools/rewrap-comments.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Rewrap over-long Zig comment lines at a column limit (default 100). + +Rules: +- Only comment-only lines are touched; trailing comments after code are left alone. +- Consecutive comment lines with the same indentation and marker (`//`, `///`, `//!`) + form a block. Blank comment lines separate paragraphs within a block. +- A line whose text starts with `- ` begins a bullet paragraph; its continuation + lines are the ones indented to align under the bullet's text (bullet lead + 2). +- Plain paragraphs join consecutive lines with the same text indentation; + wrapped lines align where the first line's text begins. +- A paragraph is rewrapped only if at least one of its lines exceeds the limit, + so deliberate short line breaks elsewhere are preserved. +""" + +import argparse +import difflib +import re +import subprocess +import sys +import textwrap + +LIMIT = 100 +COMMENT_RE = re.compile(r"^(\s*)(//[/!]?)(?:\s(.*))?$") +BULLET_RE = re.compile(r"^(\s*)- (.*)$") + + +def split_paragraphs(texts): + """texts: list of comment text (None for a bare marker line). + Returns paragraphs: dicts with lead/hang/bullet/texts/lines(indices).""" + paragraphs = [] + current = None + for index, text in enumerate(texts): + if text is None or text.strip() == "": + paragraphs.append({"literal": True, "lines": [index]}) + current = None + continue + lead = len(text) - len(text.lstrip(" ")) + bullet = BULLET_RE.match(text) + if bullet: + current = { + "lead": len(bullet.group(1)), + "hang": len(bullet.group(1)) + 2, + "bullet": True, + "texts": [bullet.group(2)], + "lines": [index], + } + paragraphs.append(current) + elif current is not None and lead == current["hang"]: + current["texts"].append(text) + current["lines"].append(index) + else: + current = { + "lead": lead, + "hang": lead, + "bullet": False, + "texts": [text], + "lines": [index], + } + paragraphs.append(current) + return paragraphs + + +def wrap_paragraph(paragraph, prefix): + joined = re.sub(r"\s+", " ", " ".join(t.strip() for t in paragraph["texts"])) + if paragraph["bullet"]: + initial = prefix + " " * paragraph["lead"] + "- " + else: + initial = prefix + " " * paragraph["lead"] + subsequent = prefix + " " * paragraph["hang"] + return textwrap.wrap( + joined, + width=LIMIT, + initial_indent=initial, + subsequent_indent=subsequent, + break_long_words=False, + break_on_hyphens=False, + ) + + +def rewrap_block(original_lines, indent, marker, texts): + prefix = indent + marker + " " + output = [] + for paragraph in split_paragraphs(texts): + block_originals = [original_lines[i] for i in paragraph["lines"]] + if paragraph.get("literal") or all(len(l) <= LIMIT for l in block_originals): + output.extend(block_originals) + else: + output.extend(wrap_paragraph(paragraph, prefix)) + return output + + +def process(source): + lines = source.split("\n") + result = [] + i = 0 + while i < len(lines): + match = COMMENT_RE.match(lines[i]) + if not match: + result.append(lines[i]) + i += 1 + continue + indent, marker = match.group(1), match.group(2) + block_lines, texts = [], [] + while i < len(lines): + m = COMMENT_RE.match(lines[i]) + if not m or m.group(1) != indent or m.group(2) != marker: + break + block_lines.append(lines[i]) + texts.append(m.group(3)) + i += 1 + result.extend(rewrap_block(block_lines, indent, marker, texts)) + return "\n".join(result) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--write", action="store_true", help="apply changes (default: diff only)") + parser.add_argument("files", nargs="*", help="files to process (default: git ls-files '*.zig')") + args = parser.parse_args() + + files = args.files or subprocess.run( + ["git", "ls-files", "*.zig"], capture_output=True, text=True, check=True + ).stdout.split() + + changed = 0 + for path in files: + with open(path, encoding="utf-8") as f: + source = f.read() + rewrapped = process(source) + if rewrapped == source: + continue + changed += 1 + if args.write: + with open(path, "w", encoding="utf-8") as f: + f.write(rewrapped) + print(f"rewrapped {path}") + else: + sys.stdout.writelines( + difflib.unified_diff( + source.splitlines(keepends=True), + rewrapped.splitlines(keepends=True), + fromfile=path, + tofile=path, + ) + ) + if not changed: + print("no comments over the limit") + + +if __name__ == "__main__": + main()