quill: add md-heading-normalize v1 with passing verification test
This commit is contained in:
1 parent
1633ac3521
commit
4ccc642253
3 files changed
+114
No files matched your search
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Normalize markdown heading styles to consistent ATX format."""
|
||||
import re
|
||||
import sys
|
||||
|
||||
_ATX_RE = re.compile(r'^(#{1,})(\s+)(.+)$')
|
||||
|
||||
def normalize(text: str) -> str:
|
||||
lines = text.splitlines()
|
||||
out = []
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.rstrip()
|
||||
next_line = lines[i + 1].rstrip() if i + 1 < len(lines) else ''
|
||||
# Setext h1/h2: only if current line is NOT already an ATX heading
|
||||
if not _ATX_RE.match(stripped):
|
||||
if re.fullmatch(r'=+\s*$', next_line):
|
||||
out.append(f'# {stripped}')
|
||||
i += 2
|
||||
continue
|
||||
if re.fullmatch(r'-+\s*$', next_line):
|
||||
out.append(f'## {stripped}')
|
||||
i += 2
|
||||
continue
|
||||
# ATX heading: cap level at 6
|
||||
m = _ATX_RE.match(stripped)
|
||||
if m:
|
||||
hashes, space, content = m.group(1), m.group(2), m.group(3)
|
||||
level = min(len(hashes), 6)
|
||||
out.append(f'{"#" * level}{space}{content}')
|
||||
i += 1
|
||||
continue
|
||||
out.append(stripped)
|
||||
i += 1
|
||||
return '\n'.join(out) + ('\n' if text.endswith('\n') else '')
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print('usage: md-heading-normalize.py <file>', file=sys.stderr)
|
||||
return 2
|
||||
path = sys.argv[1]
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
src = f.read()
|
||||
sys.stdout.write(normalize(src))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verification test: md-heading-normalize produces correct ATX output."""
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
SCRIPT = '/root/.hermes/wizard-outputs/quill/work/md-heading-normalize.py'
|
||||
|
||||
CASES = [
|
||||
# (input, expected_output)
|
||||
("Title\n=====\n\nBody.\n", "# Title\n\nBody.\n"),
|
||||
("Section\n-------\n", "## Section\n"),
|
||||
("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n####### H7\n",
|
||||
"# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n###### H7\n"),
|
||||
("plain text\n# not setext\n===\n", "plain text\n# not setext\n===\n"), # only setext if *next* line is ===
|
||||
("# H1\nsome text\n## H2\n", "# H1\nsome text\n## H2\n"),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
for idx, (src, expected) in enumerate(CASES):
|
||||
with tempfile.NamedTemporaryFile('w', suffix='.md', delete=False) as f:
|
||||
f.write(src)
|
||||
path = f.name
|
||||
try:
|
||||
result = subprocess.run([sys.executable, SCRIPT, path], capture_output=True, text=True)
|
||||
got = result.stdout
|
||||
if got == expected:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
print(f'FAIL case {idx}:\n expected={expected!r}\n got={got!r}', file=sys.stderr)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
print(f'PASS={passed} FAIL={failed}')
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Verification Report — md-heading-normalize v1
|
||||
|
||||
## Artifact
|
||||
- Script: `md-heading-normalize.py`
|
||||
- Test: `test_md_heading_normalize.py`
|
||||
|
||||
## Test Results
|
||||
```
|
||||
PASS=5 FAIL=0
|
||||
```
|
||||
|
||||
## Cases Covered
|
||||
1. Setext h1 (`Title` + `=====` → `# Title`)
|
||||
2. Setext h2 (`Section` + `-------` → `## Section`)
|
||||
3. ATX cap: `####### H7` → `###### H7`
|
||||
4. Setext skipped when current line is ATX heading
|
||||
5. Round-trip preservation of already-normalized ATX headings
|
||||
|
||||
## Command
|
||||
```
|
||||
python3 test_md_heading_normalize.py
|
||||
```
|
||||
|
||||
## Observed
|
||||
- Exit code: 0
|
||||
- 5 tests passed, 0 failed
|
||||
Reference in new issue
Block a user