153 lines
5.0 KiB
Python
Executable File
153 lines
5.0 KiB
Python
Executable File
#!/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()
|