37 per-function code blocks with prose explaining design reasoning, edge cases, and CL traps. Combined tangle blocks at end for actual compilation. New scripts/tangle.py: reliable Python tangler (emacs --batch failed). Added: %split-string, %join-lines, tangle helper. CL traps documented in org prose: - defstruct generates keyword constructors (no :constructor needed) - case with strings uses EQL — use cond + string= - CL strings: no \n escape — use (string #\Newline) - FiveAM closure capture — use list boxing - read-byte is package-locked — use read-raw-byte - ASDF compile-file stricter than LOAD — debug with LOAD 60 tests, 100% GREEN.
75 lines
2.4 KiB
Python
75 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""tangle.py — Extract code blocks from .org files into .lisp files.
|
|
|
|
Reads all .org files in org/ directory, finds #+BEGIN_SRC lisp :tangle <path>
|
|
blocks, and writes/concatenates them to the specified target paths.
|
|
|
|
Blocks with the same :tangle target are concatenated in file order.
|
|
|
|
Usage:
|
|
python3 scripts/tangle.py # tangle all org/ files
|
|
python3 scripts/tangle.py org/specific.org # tangle one file
|
|
|
|
Target paths are relative to the project root (../target from org/ = project/target).
|
|
"""
|
|
import re
|
|
import os
|
|
import sys
|
|
from collections import OrderedDict
|
|
|
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
ORG_DIR = os.path.join(PROJECT_ROOT, 'org')
|
|
|
|
def tangle_file(org_path):
|
|
"""Extract tangle blocks from one .org file."""
|
|
with open(org_path) as f:
|
|
content = f.read()
|
|
|
|
# Find all tangle blocks with their targets
|
|
pattern = r'#\+BEGIN_SRC lisp :tangle ([^\n]+)\n(.*?)\n#\+END_SRC'
|
|
blocks = re.findall(pattern, content, re.DOTALL)
|
|
|
|
if not blocks:
|
|
return 0
|
|
|
|
# Group by target path
|
|
targets = OrderedDict()
|
|
for tangle_path, code in blocks:
|
|
# Resolve tangle path: ../src/x.lisp -> src/x.lisp
|
|
resolved = tangle_path.replace('../', '')
|
|
full_path = os.path.join(PROJECT_ROOT, resolved)
|
|
if full_path not in targets:
|
|
targets[full_path] = []
|
|
targets[full_path].append(code.strip())
|
|
|
|
for full_path, codes in targets.items():
|
|
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
combined = '\n\n'.join(codes) + '\n'
|
|
with open(full_path, 'w') as f:
|
|
f.write(combined)
|
|
print(f" {os.path.relpath(full_path, PROJECT_ROOT)} ({len(codes)} blocks, {sum(len(c) for c in codes)} chars)")
|
|
|
|
return len(blocks)
|
|
|
|
def main():
|
|
if len(sys.argv) > 1:
|
|
org_files = [f for f in sys.argv[1:] if f.endswith('.org')]
|
|
else:
|
|
org_files = [os.path.join(ORG_DIR, f) for f in os.listdir(ORG_DIR) if f.endswith('.org')]
|
|
|
|
total_blocks = 0
|
|
for org_file in sorted(org_files):
|
|
name = os.path.basename(org_file)
|
|
blocks = tangle_file(org_file)
|
|
if blocks:
|
|
print(f"{name}: {blocks} blocks")
|
|
total_blocks += blocks
|
|
|
|
if total_blocks > 0:
|
|
print(f"\nTotal: {total_blocks} code blocks tangled")
|
|
else:
|
|
print("No tangle blocks found.")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|