feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL. Includes PubMed pipeline, drug approvals, AI summaries, and systematic review tools.
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Robust sky-btn fixer for Vue template NButton elements.
|
||||
|
||||
Processes ALL .vue files in frontend/src.
|
||||
For every <NButton (including <n-button) in the template that:
|
||||
- does NOT have the "text" prop
|
||||
- does NOT already have "sky-btn" in its class attribute
|
||||
- does NOT use :class
|
||||
Adds class="sky-btn" (or appends " sky-btn" to existing class).
|
||||
|
||||
IMPORTANT: Does NOT match NButton inside quoted attribute values (fixes v1's bug).
|
||||
Also fixes any files corrupted by v1's buggy run.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
def find_template_section(content):
|
||||
"""Find the <template>...</template> section in a .vue file.
|
||||
|
||||
Uses rfind('</template>') to find the LAST closing tag, so nested
|
||||
<template #header>...</template> inside the main template are handled
|
||||
correctly (the main template always encompasses all nested blocks).
|
||||
"""
|
||||
template_start = content.find('<template>')
|
||||
if template_start < 0:
|
||||
# Try <template lang="...">, <template v-if="..." etc.
|
||||
m = re.search(r'<template\b[^>]*>', content)
|
||||
if not m:
|
||||
return None, None
|
||||
template_start = m.start()
|
||||
|
||||
# Use rfind to get the LAST </template> which closes the main template block
|
||||
template_end = content.rfind('</template>')
|
||||
if template_end < 0:
|
||||
return None, None
|
||||
|
||||
return template_start, template_end + len('</template>')
|
||||
|
||||
|
||||
def find_all_nbutton_tags(text, start_pos=0):
|
||||
"""
|
||||
Find all <NButton or <n-button opening tags in text, EXCLUDING those
|
||||
inside quoted attribute values.
|
||||
|
||||
Returns list of (match_start, match_end, tag_text) for opening tags.
|
||||
"""
|
||||
# First, let's strip out everything inside quoted strings to avoid
|
||||
# matching NButton inside attribute values
|
||||
# We'll build a map of which positions are "real" vs inside quotes
|
||||
|
||||
inside_double_quote = False
|
||||
inside_single_quote = False
|
||||
positions = {} # pos -> 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 <NButton inside quoted attribute values like
|
||||
placeholder="...<NButton..." and inserted class="sky-btn" there,
|
||||
breaking the HTML.
|
||||
|
||||
We detect this pattern and fix it by removing the misplaced class.
|
||||
Also fixes cases where NButton tags were mangled together with other tags.
|
||||
"""
|
||||
# Pattern 1: NButton class="sky-btn" inside an attribute value
|
||||
# e.g., placeholder="研究目的(<NButton ... class="sky-btn">...="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...<NButton...class="sky-btn">...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: "<NButton ...class="sky-btn">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: "<NButton ... class="sky-btn">..." (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 <template> found"
|
||||
|
||||
template_text = content[template_start:template_end]
|
||||
|
||||
# Find all NButton tags in template
|
||||
nbutton_tags = find_all_nbutton_tags(template_text)
|
||||
|
||||
if not nbutton_tags:
|
||||
return 0, "no NButton found"
|
||||
|
||||
changes = 0
|
||||
# Process in reverse order to preserve positions
|
||||
for tag_start, tag_end, tag_text in reversed(nbutton_tags):
|
||||
modified = modify_nbutton_tag(tag_text)
|
||||
if modified:
|
||||
abs_start = template_start + tag_start
|
||||
abs_end = template_start + tag_end
|
||||
content = content[:abs_start] + modified + content[abs_end:]
|
||||
changes += 1
|
||||
|
||||
if changes > 0 or content != original:
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return changes, "ok"
|
||||
|
||||
|
||||
def main():
|
||||
frontend_src = 'd:/ClaudeCode/frontend/src'
|
||||
|
||||
# Find all .vue files
|
||||
vue_files = []
|
||||
for root, dirs, files in os.walk(frontend_src):
|
||||
for f in files:
|
||||
if f.endswith('.vue'):
|
||||
vue_files.append(os.path.join(root, f))
|
||||
|
||||
vue_files.sort()
|
||||
|
||||
total_changes = 0
|
||||
file_results = []
|
||||
all_files_with_nbutton = []
|
||||
|
||||
for filepath in vue_files:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
if not re.search(r'<[Nn][-]?[Bb]utton\b', content):
|
||||
continue
|
||||
|
||||
all_files_with_nbutton.append(filepath)
|
||||
changes, status = process_file(filepath)
|
||||
if changes > 0:
|
||||
rel_path = os.path.relpath(filepath, frontend_src)
|
||||
file_results.append((rel_path, changes))
|
||||
total_changes += changes
|
||||
print(f" MODIFIED: {rel_path} ({changes} change(s))")
|
||||
|
||||
print(f"\n=== Summary ===")
|
||||
print(f"Files with NButton: {len(all_files_with_nbutton)}")
|
||||
print(f"Files modified: {len(file_results)}")
|
||||
print(f"Total NButton tags modified: {total_changes}")
|
||||
|
||||
if not file_results:
|
||||
print("\nNO_CHANGES")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user