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>
171 lines
5.4 KiB
Python
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()
|