#!/usr/bin/env python3 """Parse a .pd file into JSON for Conftest/Rego validation.""" import json, sys, re from pathlib import Path def parse_pd(filepath): with open(filepath) as f: text = f.read() lines = text.split("\n") records = [] x_index = 0 errors = [] for i, raw in enumerate(lines): line = raw.strip() if not line or line == ";": continue if not line.startswith("#"): errors.append(f"line {i+1}: expected # prefix") continue raw = line[1:].strip() if not raw.endswith(";"): errors.append(f"line {i+1}: missing terminating semicolon") continue body = raw[:-1].strip() if line.startswith("#N"): nbody = body[1:].strip() # remove 'N' parts = nbody.split(None, 1) if parts[0] == "canvas": tokens = parts[1].split() rec = { "type": "canvas", "x": safe_int(tokens[0]), "y": safe_int(tokens[1]), "width": safe_int(tokens[2]), "height": safe_int(tokens[3]), "name": tokens[4] if len(tokens) > 4 else "", } records.append(rec) else: records.append({"type": "new", "data": rest}) elif line.startswith("#A"): values = [safe_float(v) for v in rest.split()] records.append({"type": "array_data", "values": values}) elif line.startswith("#X connect"): xbody = body[1:].strip() elems = xbody.split() if len(elems) >= 5: rec = { "type": "connect", "x_index": x_index, "src": safe_int(elems[1]), "outlet": safe_int(elems[2]), "dst": safe_int(elems[3]), "inlet": safe_int(elems[4]), } records.append(rec) # Connect records do NOT increment Pd's object index elif line.startswith("#X obj"): xbody = body[1:].strip() # xbody = "obj 20 20 notein" elem, x_str, y_str, rest = xbody.split(None, 3) obj_parts = rest.split() rec = { "type": "obj", "x_index": x_index, "x": safe_int(x_str), "y": safe_int(y_str), "name": obj_parts[0] if obj_parts else "", "args": obj_parts[1:], } records.append(rec) x_index += 1 elif line.startswith("#X msg"): xbody = body[1:].strip() elem, x_str, y_str, content = xbody.split(None, 3) records.append({ "type": "msg", "x_index": x_index, "x": safe_int(x_str), "y": safe_int(y_str), "content": content, }) x_index += 1 elif line.startswith("#X floatatom"): xbody = body[1:].strip() elems = xbody.split() rec = {"type": "floatatom", "x_index": x_index, "x": safe_int(elems[1]), "y": safe_int(elems[2])} if len(elems) > 3: rec["width"] = safe_int(elems[3]) if len(elems) > 4: rec["min"] = safe_float(elems[4]) if len(elems) > 5: rec["max"] = safe_float(elems[5]) records.append(rec) x_index += 1 elif line.startswith("#X symbolatom"): records.append({"type": "symbolatom", "x_index": x_index}) x_index += 1 elif line.startswith("#X text"): xbody = body[1:].strip() records.append({"type": "text", "x_index": x_index, "content": xbody}) x_index += 1 elif line.startswith("#X restore"): xbody = body[1:].strip() records.append({"type": "restore", "x_index": x_index, "data": xbody}) x_index += 1 elif line.startswith("#X coords"): xbody = body[1:].strip() records.append({"type": "coords", "x_index": x_index, "data": xbody}) x_index += 1 elif line.startswith("#X array"): xbody = body[1:].strip() records.append({"type": "array", "x_index": x_index, "data": xbody}) x_index += 1 else: errors.append(f"line {i+1}: unknown record type: {line[:60]}") return { "file": str(filepath), "x_record_count": x_index, "records": records, "parse_errors": errors, } def safe_int(s): try: # Handle Pd special values like "-" for empty if s == "-": return 0 return int(s) except (ValueError, IndexError): return 0 def safe_float(s): try: if s == "-": return 0.0 return float(s) except (ValueError, IndexError): return 0.0 if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: pd2json.py [file2.pd ...]") sys.exit(1) results = [] for arg in sys.argv[1:]: results.append(parse_pd(arg)) print(json.dumps(results if len(results) > 1 else results[0], indent=2))