#!/usr/bin/env python3 """ migrate_fa_to_lucide.py — Replace FontAwesome with Lucide components. Usage: python3 scripts/migrate_fa_to_lucide.py src/routes/events/[event_id]/\(pres_mgmt\)/ Skips content inside HTML comments. Adds/merges lucide-svelte imports. """ import re import sys import os from pathlib import Path # ── FA icon → Lucide component name ───────────────────────────────────────── FA_TO_LUCIDE = { 'fa-spinner': 'LoaderCircle', 'fa-cog': 'LoaderCircle', # only when fa-spin 'fa-sync-alt': 'RefreshCw', 'fa-times': 'X', 'fa-exclamation-triangle': 'TriangleAlert', 'fa-check': 'Check', 'fa-check-circle': 'CheckCircle', 'fa-plus': 'Plus', 'fa-minus': 'Minus', 'fa-save': 'Save', 'fa-edit': 'Pencil', 'fa-eye': 'Eye', 'fa-eye-slash': 'EyeOff', 'fa-toggle-on': 'ToggleRight', 'fa-toggle-off': 'ToggleLeft', 'fa-star-of-life': 'Asterisk', 'fa-id-card': 'IdCard', 'fa-paper-plane': 'Send', 'fa-map-marker-alt': 'MapPin', 'fa-file-alt': 'FileText', 'fa-envelope': 'Mail', 'fa-book': 'BookOpen', 'fa-angle-right': 'ChevronRight', 'fa-user': 'User', 'fa-tasks': 'ListChecks', 'fa-plane': 'Plane', 'fa-list': 'List', 'fa-link': 'Link', 'fa-file-archive': 'Archive', 'fa-comment-dots': 'MessageCircle', 'fa-chevron-up': 'ChevronUp', 'fa-chevron-down': 'ChevronDown', 'fa-camera': 'Camera', 'fa-barcode': 'Barcode', 'fa-upload': 'Upload', 'fa-search': 'Search', 'fa-mail-bulk': 'Mails', 'fa-laptop-code': 'Laptop', 'fa-copy': 'Copy', 'fa-user-tag': 'Tag', 'fa-user-secret': 'UserRound', 'fa-users': 'Users', 'fa-user-circle': 'CircleUser', 'fa-sort': 'ArrowUpDown', 'fa-question': 'HelpCircle', 'fa-map-marked': 'MapPinned', 'fa-list-ol': 'ListOrdered', 'fa-laptop': 'Laptop', 'fa-info': 'Info', 'fa-building': 'Building2', 'fa-user-slash': 'UserX', 'fa-user-check': 'UserCheck', 'fa-unlink': 'Unlink', 'fa-star': 'Star', 'fa-search-location': 'MapPin', 'fa-remove-format': 'RemoveFormatting', 'fa-qrcode': 'QrCode', 'fa-key': 'Key', 'fa-heartbeat': 'HeartPulse', 'fa-hat-wizard': 'Wand2', 'fa-fingerprint': 'Fingerprint', 'fa-file-csv': 'FileSpreadsheet', 'fa-file': 'File', 'fa-clock': 'Clock', 'fa-clipboard-list': 'ClipboardList', 'fa-chart-line': 'TrendingUp', 'fa-chalkboard-teacher': 'Presentation', 'fa-calendar-day': 'CalendarDays', 'fa-bell-slash': 'BellOff', 'fa-bell': 'Bell', # ── Additional mappings ────────────────────────────────────────────────── 'fa-arrow-left': 'ArrowLeft', 'fa-arrow-right': 'ArrowRight', 'fa-arrow-up': 'ArrowUp', 'fa-arrow-down': 'ArrowDown', 'fa-ban': 'Ban', 'fa-broom': 'Trash2', # closest semantic match 'fa-calendar-alt': 'CalendarDays', 'fa-database': 'Database', 'fa-door-open': 'DoorOpen', 'fa-download': 'Download', 'fa-exchange-alt': 'ArrowLeftRight', 'fa-file-image': 'FileImage', 'fa-lock': 'Lock', 'fa-magic': 'Sparkles', 'fa-print': 'Printer', 'fa-sticky-note': 'StickyNote', 'fa-sync': 'RefreshCw', 'fa-tag': 'Tag', 'fa-trash': 'Trash2', 'fa-user-ninja': 'UserRound', 'fa-user-tie': 'UserRound', 'fa-video': 'Video', 'fa-archive': 'Archive', 'fa-link-slash': 'Unlink', 'fa-question-circle': 'HelpCircle', # ── Additional unmapped icons ────────────────────────────────────────────── 'fa-compress-arrows-alt':'Minimize2', 'fa-expand-arrows-alt': 'Maximize2', 'fa-secret': 'ShieldCheck', 'fa-user-shield': 'ShieldUser', 'fa-user-nurse': 'UserRound', 'fa-user-friends': 'Users', 'fa-user-plus': 'UserPlus', 'fa-user-edit': 'UserRoundPen', 'fa-palette': 'Palette', 'fa-eraser': 'Eraser', 'fa-code': 'Code', 'fa-lock-open': 'LockOpen', 'fa-unlock': 'LockOpen', 'fa-trash-alt': 'Trash2', 'fa-folder-open': 'FolderOpen', 'fa-minus-circle': 'MinusCircle', 'fa-plus-circle': 'PlusCircle', 'fa-window-close': 'X', 'fa-cut': 'Scissors', 'fa-caret-down': 'ChevronDown', 'fa-caret-right': 'ChevronRight', 'fa-cogs': 'Settings2', 'fa-phone': 'Phone', 'fa-phone-slash': 'PhoneOff', 'fa-flag': 'Flag', 'fa-calendar-week': 'CalendarDays', 'fa-address-book': 'BookUser', 'fa-info-circle': 'Info', 'fa-comment-slash': 'MessageX', 'fa-paperclip': 'Paperclip', 'fa-keyboard': 'Keyboard', 'fa-crosshairs': 'Crosshair', 'fa-redo': 'RotateCcw', 'fa-tools': 'Wrench', 'fa-video-slash': 'VideoOff', 'fa-home': 'House', 'fa-calendar': 'Calendar', 'fa-check-square': 'SquareCheck', 'fa-square': 'Square', 'fa-times-circle': 'CircleX', 'fa-undo': 'RotateCcw', 'fa-trash-restore': 'ArchiveRestore', 'fa-lock-open': 'LockOpen', 'fa-compress': 'Minimize2', 'fa-expand': 'Maximize2', 'fa-grip-lines': 'GripHorizontal', 'fa-bars': 'Menu', 'fa-refresh': 'RefreshCw', } # Skip modifiers — not real icon names FA_MODIFIERS = {'fas', 'far', 'fab', 'fa-spin', 'fa-fw', 'fa-lg', 'fa-2x', 'fa-sm'} # ── Pattern: ─────────── # [^>]* matches newlines too (character class, not dot) SPAN_RE = re.compile( r']*>\s*' ) # ── Comment splitter ───────────────────────────────────────────────────────── COMMENT_RE = re.compile(r'()') # ── Lucide import line ──────────────────────────────────────────────────────── IMPORT_RE = re.compile(r"import\s*\{([^}]+)\}\s*from\s*'@lucide/svelte'\s*;?") def parse_fa_class(class_str): """Return (icon_name, extra_classes, has_spin) from a FA class string.""" parts = class_str.split() icon_name = None has_spin = 'fa-spin' in parts extra = [] for p in parts: if p in FA_MODIFIERS: continue elif p.startswith('fa-'): if icon_name is None: icon_name = p # first real icon name wins extra.append(p) return icon_name, extra, has_spin def replace_span(m): """Regex sub callback: replace a single FA span with a Lucide component.""" """ if icon_name is None: return m.group(0) lucide = FA_TO_LUCIDE.get(icon_name) if lucide is None: print(f' ⚠ no mapping for {icon_name!r} — left as-is', file=sys.stderr) return m.group(0) classes = extra[:] if has_spin: classes.append('animate-spin') class_attr = f' class="{" ".join(classes)}"' if classes else '' return f'<{lucide} size="1em"{class_attr} />' def process_content(content): """Replace FA spans, skip HTML comments. Return (new_content, used_icons).""" used_icons = set() def track_and_replace(m): result = replace_span(m) if result != m.group(0): # Extract lucide name from result lucide_name = result.split()[0].lstrip('<') used_icons.add(lucide_name) return result # Split by comments; only process non-comment segments parts = COMMENT_RE.split(content) new_parts = [] for part in parts: if part.startswith('