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>
This commit is contained in:
170
scripts/strip_names.py
Normal file
170
scripts/strip_names.py
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user