""" Robust sky-btn fixer for Vue template NButton elements. Processes ALL .vue files in frontend/src. For every ... section in a .vue file. Uses rfind('') to find the LAST closing tag, so nested inside the main template are handled correctly (the main template always encompasses all nested blocks). """ template_start = content.find('') def find_all_nbutton_tags(text, start_pos=0): """ Find all True if real (not in quotes) i = start_pos while i < len(text): ch = text[i] if ch == '"' and not inside_single_quote: inside_double_quote = not inside_double_quote elif ch == "'" and not inside_double_quote: inside_single_quote = not inside_single_quote elif ch == '\\' and i + 1 < len(text): # Skip escaped characters positions[i] = not (inside_double_quote or inside_single_quote) i += 2 continue positions[i] = not (inside_double_quote or inside_single_quote) i += 1 # Now find all NButton opening tags with proper regex # Only consider positions where the '<' is not inside quotes tag_pattern = re.compile(r'<[Nn][-]?[Bb]utton\b', re.DOTALL) results = [] for match in tag_pattern.finditer(text, start_pos): tag_start = match.start() # Skip if the '<' is inside quotes if tag_start in positions and not positions[tag_start]: continue # Now find the end of this opening tag (the '>' that closes it) # Need to handle nested quotes within the tag pos = match.end() in_dq = False in_sq = False while pos < len(text): ch = text[pos] if ch == '"' and not in_sq: in_dq = not in_dq elif ch == "'" and not in_dq: in_sq = not in_sq elif ch == '>' and not in_dq and not in_sq: tag_end = pos + 1 tag_text = text[tag_start:tag_end] results.append((tag_start, tag_end, tag_text)) break pos += 1 return results def has_text_prop(tag_text): """Check if the NButton tag has 'text' as a boolean prop (not :text).""" # Remove all quoted strings stripped = re.sub(r'"[^"]*"', '', tag_text) stripped = re.sub(r"'[^']*'", '', stripped) # Look for 'text' as a standalone word not preceded by ':' for m in re.finditer(r'\btext\b', stripped): if m.start() > 0 and stripped[m.start()-1] == ':': continue # :text, skip # Must be preceded by whitespace or start of tag pre_char = stripped[m.start()-1] if m.start() > 0 else ' ' if pre_char in (' ', '\t', '\n', '\r', '>'): return True return False def has_class_binding(tag_text): """Check if the NButton has :class.""" # Remove quoted strings first stripped = re.sub(r'"[^"]*"', '', tag_text) stripped = re.sub(r"'[^']*'", '', stripped) if re.search(r':class\b', stripped): return True # Also check for v-bind:class if re.search(r'v-bind:class\b', stripped): return True return False def has_skybtn(tag_text): """Check if the NButton already has sky-btn in class.""" return 'sky-btn' in tag_text def modify_nbutton_tag(tag_text): """Add class="sky-btn" to an NButton tag. Returns modified tag or None.""" # Rules if has_text_prop(tag_text): return None if has_class_binding(tag_text): return None if has_skybtn(tag_text): return None # Check if it has class="..." class_match = re.search(r'class\s*=\s*"([^"]*)"', tag_text) if class_match: existing = class_match.group(1) if 'sky-btn' in existing: return None new_class = existing + ' sky-btn' if existing.strip() else 'sky-btn' result = tag_text[:class_match.start(1)] + new_class + tag_text[class_match.end(1):] return result # No class attribute - add class="sky-btn" # Insert before the closing > if tag_text.endswith('/>') or tag_text.endswith('/>'): return tag_text[:-2] + ' class="sky-btn" />' elif tag_text.endswith('>'): return tag_text[:-1] + ' class="sky-btn">' return None def fix_corrupted_file(content): """ Fix files corrupted by v1 of the fixer. v1's bug: it matched ...="createReview"> # Fix: remove the corrupted inner NButton artifacts # Pattern 2: Duplicate/split attributes from v1 mangling # e.g., class="sky-btn">freshCitations"> should be class="sky-btn"> # and the rest should be proper attributes original = content # Fix pattern: something="text......some garbage>" # This happens when v1 matched NButton inside a placeholder or other attribute pattern1 = re.compile( r'(placeholder|title|description|label|help-text|filter)\s*=\s*"([^"]*?)(<[Nn][-]?[Bb]utton\b[^>]*?class\s*=\s*"sky-btn"[^>]*>)([^"]*?)"', re.DOTALL ) content = pattern1.sub(r'\1="\2sky-btn-placeholder-fix\4"', content) # Fix pattern: "n size="..." ...>" # This happens when v1 mangled a tag name prefix pattern2 = re.compile( r'(<[Nn][-]?[Bb]utton\b[^>]*?class\s*=\s*"sky-btn">)\s*([a-zA-Z]+)\s*=\s*"', re.DOTALL ) # Remove attribute fragments that somehow got after the closing > content = pattern2.sub(r'\1 ', content) # Fix pattern: "..." (where "..." is a truncated/mangled attribute) pattern3 = re.compile( r'(<[Nn][-]?[Bb]utton\b[^>]*?class\s*=\s*"sky-btn"[^>]*>)([a-zA-Z-]+)\s*=\s*"', re.DOTALL ) content = pattern3.sub(r'\1', content) return content def process_file(filepath): """Process a single .vue file.""" with open(filepath, 'r', encoding='utf-8') as f: content = f.read() original = content # First, try to fix any corruption from v1 content = fix_corrupted_file(content) # Find template section template_start, template_end = find_template_section(content) if template_start is None: return 0, "no