Files
rose-ash/scripts/strip_names.py
giles 6528ce78b9 Scripts: page migration helpers for one-per-file layout
Python + shell tooling used to split grouped index.sx files into
one-directory-per-page layout (see the hyperscript gallery migration).
name-mapping.json records the rename table; strip_names.py is a helper
for extracting component names from .sx sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:09:15 +00:00

171 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Phase 2: Strip names from definition forms in one-per-file .sx files.
Transforms:
(defcomp ~name (params) body) -> (defcomp (params) body)
(define name value) -> (define value)
(define (name args) body) -> (define (args) body)
(defpage name :path ...) -> (defpage :path ...)
etc.
Self-named components (where the name is part of the content) are left as-is
if they use a different naming convention than the file path would give.
Usage:
python3 scripts/strip_names.py --dry-run # preview
python3 scripts/strip_names.py # execute
"""
import os
import sys
import re
import argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from shared.sx.parser import parse_all
from shared.sx.types import Symbol, Keyword
NAMED_DEFS = {"defcomp", "defisland", "defmacro", "defpage",
"defhandler", "defstyle", "deftype", "defeffect",
"defrelation", "deftest"}
SKIP_FILES = {"boundary.sx", "_init.sx", "_preamble.sx"}
def strip_name_from_source(source):
"""Strip the name from a definition in raw source text.
Returns (new_source, old_name, def_type) or None if no change needed.
Works on raw text to preserve formatting, comments, etc.
Uses the parser to understand structure, then does targeted text surgery.
"""
try:
exprs = parse_all(source)
except Exception:
return None
if not exprs:
return None
expr = exprs[0]
if not isinstance(expr, list) or not expr or not isinstance(expr[0], Symbol):
return None
kw = expr[0].name
if kw in NAMED_DEFS:
if len(expr) < 2 or not isinstance(expr[1], Symbol):
return None # Already unnamed or malformed
old_name = expr[1].name
# Find and remove the name symbol from source text.
# Pattern: (defXXX ~name or (defXXX name
# We need to find the name token after the keyword and remove it.
pattern = re.compile(
r'(\(\s*' + re.escape(kw) + r')\s+' + re.escape(old_name) + r'(?=\s)',
re.DOTALL
)
match = pattern.search(source)
if match:
new_source = source[:match.end(1)] + source[match.end():]
return (new_source, old_name, kw)
return None
if kw == "define":
if len(expr) < 2:
return None
name_part = expr[1]
if isinstance(name_part, Symbol):
old_name = name_part.name
# (define name value) -> (define value)
pattern = re.compile(
r'(\(\s*define)\s+' + re.escape(old_name) + r'(?=\s)',
re.DOTALL
)
match = pattern.search(source)
if match:
new_source = source[:match.end(1)] + source[match.end():]
return (new_source, old_name, "define")
elif (isinstance(name_part, list) and name_part
and isinstance(name_part[0], Symbol)):
old_name = name_part[0].name
# (define (name args...) body) -> (define (args...) body)
# Find (define (name and remove just "name "
pattern = re.compile(
r'(\(\s*define\s+\()\s*' + re.escape(old_name) + r'\s*',
re.DOTALL
)
match = pattern.search(source)
if match:
new_source = source[:match.end(1)] + source[match.end():]
return (new_source, old_name, "define")
return None
return None
def main():
parser = argparse.ArgumentParser(description="Strip names from definitions")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--dir", default=None)
args = parser.parse_args()
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(project_root)
dirs = [args.dir] if args.dir else ["sx/sx", "sx/sxc"]
stripped = 0
skipped = 0
errors = []
for d in dirs:
if not os.path.isdir(d):
continue
for root, subdirs, files in os.walk(d):
subdirs[:] = [x for x in subdirs if x not in ('__pycache__', '.cache')]
for filename in sorted(files):
if not filename.endswith('.sx') or filename in SKIP_FILES:
continue
filepath = os.path.join(root, filename)
try:
with open(filepath, encoding='utf-8') as f:
source = f.read()
except Exception as e:
errors.append((filepath, str(e)))
continue
result = strip_name_from_source(source)
if result is None:
skipped += 1
continue
new_source, old_name, def_type = result
if args.dry_run:
print(f" {filepath}: strip {def_type} {old_name}")
stripped += 1
continue
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_source)
stripped += 1
print(f"\n{'Dry run — would strip' if args.dry_run else 'Stripped'} {stripped} names")
print(f"Skipped {skipped} (already unnamed or no definition)")
if errors:
print(f"Errors: {len(errors)}")
for fp, e in errors:
print(f" {fp}: {e}")
if __name__ == "__main__":
main()