Utensil's Zettelkasten-style forest of evergreen notes on math and tech.
utensil.tngl.sh/forest/
1#!/usr/bin/env -S uv run
2# /// script
3# requires-python = ">=3.11,<3.12"
4# dependencies = ["termcolor"]
5# ///
6
7import re
8from pathlib import Path
9from typing import List, Tuple
10import sys
11from termcolor import colored
12
13def find_method_syntax_in_content(content: str) -> Tuple[List[str], List[str]]:
14 """Find all method definitions and calls in content"""
15 # Match method definitions like [draw/something]{ but not \[ escaped ones
16 def_pattern = r'(?<!\\)\[([a-zA-Z0-9_-]+)/([a-zA-Z0-9_/-]+)\]\s*\{'
17 definitions = re.findall(def_pattern, content)
18
19 # Match method calls like #draw/something or #draw/sb/sth
20 call_pattern = r'#([a-zA-Z0-9_-]+)/([a-zA-Z0-9_/-]+)'
21 calls = re.findall(call_pattern, content)
22
23 return definitions, calls
24
25def replace_method_syntax(content: str) -> str:
26 """Replace method syntax with dashes in content"""
27 # Replace definitions [draw/something]{ -> [draw-something]{ but preserve \[
28 content = re.sub(r'(?<!\\)\[([a-zA-Z0-9_-]+)/([a-zA-Z0-9_/-]+)\]\s*\{',
29 lambda m: f'[{m.group(1)}-{m.group(2).replace("/", "-")}] {{',
30 content)
31
32 # Replace calls #draw/something -> #draw-something
33 content = re.sub(r'#([a-zA-Z0-9_-]+)/([a-zA-Z0-9_/-]+)',
34 lambda m: f'#{m.group(1)}-{m.group(2).replace("/", "-")}',
35 content)
36
37 return content
38
39def process_file(file_path: Path, dry_run: bool = True) -> None:
40 """Process a single file, showing changes in dry-run mode"""
41 try:
42 with open(file_path, 'r', encoding='utf-8') as f:
43 content = f.read()
44
45 definitions, calls = find_method_syntax_in_content(content)
46
47 if not definitions and not calls:
48 return
49
50 print(colored(f"\nProcessing {file_path}:", 'blue'))
51 print(colored(f"Found {len(definitions)} definitions and {len(calls)} calls", 'cyan'))
52
53 for prefix, method in definitions:
54 old = f'[{prefix}/{method}]'
55 new = f'[{prefix}-{method.replace("/", "-")}]'
56 print(f"Definition: {colored(old, 'red')} -> {colored(new, 'green')}")
57
58 for prefix, method in calls:
59 old = f'#{prefix}/{method}'
60 new = f'#{prefix}-{method.replace("/", "-")}'
61 print(f"Call: {colored(old, 'red')} -> {colored(new, 'green')}")
62
63 if not dry_run:
64 new_content = replace_method_syntax(content)
65 with open(file_path, 'w', encoding='utf-8') as f:
66 f.write(new_content)
67 print(colored("File updated", 'green'))
68 else:
69 print(colored("Dry run - no changes made", 'yellow'))
70
71 except Exception as e:
72 print(colored(f"Error processing {file_path}: {str(e)}", 'red'))
73
74def process_files(dry_run: bool = True) -> None:
75 """Process all matching files in the trees directory"""
76 for file_path in Path('trees').glob('*g-*.tree'):
77 process_file(file_path, dry_run)
78
79def main():
80 dry_run = '--apply' not in sys.argv
81 print(colored(f"Running in {'dry-run' if dry_run else 'apply'} mode",
82 'magenta' if dry_run else 'green'))
83 process_files(dry_run)
84
85if __name__ == '__main__':
86 main()