2929UNIQUE_ATTR = SORTING .index ({"id" , "ref" , "key" })
3030
3131
32- def format_python_expression (expr : str ) -> str :
32+ def collect_expressions (node ) -> list [str ]:
33+ """
34+ Collect all Python expressions from a template node tree.
35+
36+ Returns a list of expressions that need formatting.
37+ """
38+ expressions = []
39+
40+ def _collect_from_node (n ):
41+ # Skip text and comments
42+ if isinstance (n , (Comment , TextElement )):
43+ return
44+
45+ # Collect expressions from attributes
46+ if attributes := getattr (n , "attrs" , None ):
47+ for key , value in attributes .items ():
48+ if (
49+ key .startswith ((":" , "@" , "v-" ))
50+ and isinstance (value , str )
51+ and value .strip ()
52+ ):
53+ if key .startswith ("v-for" ):
54+ # Split v-for into target and iterable
55+ expr = value .strip ()
56+ parts = expr .split (" in " , 1 )
57+ if len (parts ) == 2 :
58+ expressions .append (parts [0 ].strip ())
59+ expressions .append (parts [1 ].strip ())
60+ else :
61+ expressions .append (value .strip ())
62+
63+ # Recurse into children
64+ if children := getattr (n , "children" , None ):
65+ for child in children :
66+ _collect_from_node (child )
67+
68+ _collect_from_node (node )
69+ return expressions
70+
71+
72+ def batch_format_expressions (expressions : list [str ]) -> dict [str , str ]:
73+ """
74+ Format multiple Python expressions in a single ruff call.
75+
76+ Args:
77+ expressions: List of Python expressions to format
78+
79+ Returns:
80+ Dictionary mapping original expression to formatted expression
81+ """
82+ if not expressions :
83+ return {}
84+
85+ # Filter out empty expressions
86+ non_empty = [e for e in expressions if e and e .strip ()]
87+ if not non_empty :
88+ return {}
89+
90+ try :
91+ # Create a batch of assignments
92+ lines = [f"__e{ i } __ = { expr } " for i , expr in enumerate (non_empty )]
93+ batch_source = "\n " .join (lines )
94+
95+ # Format all at once
96+ formatted_batch = run_ruff_format (
97+ batch_source , use_single_quotes = True , skip_import_sort = True
98+ )
99+
100+ # Parse the results - need to handle multiline expressions
101+ result_map = {}
102+ formatted_lines = formatted_batch .strip ().split ("\n " )
103+ formatted_lines_count = len (formatted_lines )
104+
105+ for i , expr in enumerate (non_empty ):
106+ # Find the formatted expression for this variable
107+ prefix = f"__e{ i } __ = "
108+
109+ # Find the start line
110+ start_idx = None
111+ for idx , line in enumerate (formatted_lines ):
112+ if line .startswith (prefix ):
113+ start_idx = idx
114+ break
115+
116+ if start_idx is None :
117+ continue
118+
119+ # Find the end line: next line that starts with __e (next assignment)
120+ end_idx = formatted_lines_count
121+ for idx in range (start_idx + 1 , formatted_lines_count ):
122+ line = formatted_lines [idx ]
123+ if line .startswith ("__e" ) and " = " in line :
124+ end_idx = idx
125+ break
126+
127+ # Extract the expression (everything after "variable = ")
128+ first_line = formatted_lines [start_idx ][len (prefix ) :]
129+ if start_idx + 1 < end_idx :
130+ # Multiline expression
131+ remaining_lines = formatted_lines [start_idx + 1 : end_idx ]
132+ formatted_expr = first_line + "\n " + "\n " .join (remaining_lines )
133+ else :
134+ # Single line expression
135+ formatted_expr = first_line
136+
137+ result_map [expr ] = formatted_expr
138+
139+ return result_map
140+
141+ except Exception as e :
142+ logger .debug (f"Batch formatting failed: { e } " )
143+ # Return empty dict - will fall back to individual formatting
144+ return {}
145+
146+
147+ def format_python_expression (
148+ expr : str , expression_cache : dict [str , str ] | None = None
149+ ) -> str :
33150 """
34151 Format a Python expression using ruff with single quotes.
35152
36153 Args:
37154 expr: The Python expression as a string
155+ expression_cache: Optional cache of pre-formatted expressions
38156
39157 Returns:
40158 Formatted expression as a string (using single quotes)
41159 """
42160 if not expr or not expr .strip ():
43161 return expr
44162
163+ # Check cache first
164+ if expression_cache is not None and expr in expression_cache :
165+ return expression_cache [expr ]
166+
45167 try :
168+ prefix = "__dummy__ = "
46169 # Wrap the expression in a dummy assignment to make it valid Python
47170 # This allows ruff to format it properly
48- wrapped = f"__dummy__ = { expr } "
171+ wrapped = f"{ prefix } { expr } "
49172
50173 # Format with ruff using single quotes
51174 # Skip import sorting since template expressions don't have imports
@@ -55,7 +178,6 @@ def format_python_expression(expr: str) -> str:
55178
56179 # Extract the expression back out
57180 # (remove "__dummy__ = " and trailing newline)
58- prefix = "__dummy__ = "
59181 if formatted .startswith (prefix ):
60182 formatted_expr = formatted [len (prefix ) :].rstrip ("\n " )
61183 return formatted_expr
@@ -68,7 +190,9 @@ def format_python_expression(expr: str) -> str:
68190 return expr
69191
70192
71- def format_list_expression (expr : str ) -> str :
193+ def format_list_expression (
194+ expr : str , expression_cache : dict [str , str ] | None = None
195+ ) -> str :
72196 """
73197 Format a Python list expression (v-for syntax) using ruff with single quotes.
74198
@@ -77,6 +201,7 @@ def format_list_expression(expr: str) -> str:
77201
78202 Args:
79203 expr: The list expression as a string (e.g., "item in items")
204+ expression_cache: Optional cache of pre-formatted expressions
80205
81206 Returns:
82207 Formatted expression as a string (using single quotes)
@@ -99,8 +224,10 @@ def format_list_expression(expr: str) -> str:
99224 target , iterable = parts
100225
101226 # Format each part using format_python_expression
102- formatted_target = format_python_expression (target .strip ())
103- formatted_iterable = format_python_expression (iterable .strip ())
227+ formatted_target = format_python_expression (target .strip (), expression_cache )
228+ formatted_iterable = format_python_expression (
229+ iterable .strip (), expression_cache
230+ )
104231
105232 return f"{ formatted_target } in { formatted_iterable } "
106233
@@ -111,12 +238,20 @@ def format_list_expression(expr: str) -> str:
111238
112239def format_template (template_node ) -> tuple [list [str ], tuple [int , int ]]:
113240 """
114- Returns formatted node and the original location of the template node
241+ Returns formatted node and the original location of the template node.
242+
243+ Uses batch formatting for all Python expressions to improve performance.
115244 """
116245 # Find beginning and end of script block
117246 start , end = template_node .location [0 ] - 1 , template_node .end [0 ]
118247
119- result = format_node (template_node , depth = 0 )
248+ # Collect all expressions and batch format them
249+ expressions = collect_expressions (template_node )
250+ expression_cache = batch_format_expressions (expressions ) if expressions else None
251+
252+ # Format the node tree using the cached expressions
253+ result = format_node (template_node , depth = 0 , expression_cache = expression_cache )
254+
120255 # Replace all tabs with the default indent and add line breaks
121256 tab = "\t "
122257 result = [f"{ line .replace (tab , INDENT )} \n " for line in result ]
@@ -160,7 +295,9 @@ def sort_attr(attr):
160295 return f"{ len (SORTING )} { attr } "
161296
162297
163- def format_attribute (key , value , indent = "" , single_attribute = True ):
298+ def format_attribute (
299+ key , value , indent = "" , single_attribute = True , expression_cache = None
300+ ):
164301 """
165302 Format an attribute key-value pair.
166303
@@ -176,10 +313,10 @@ def format_attribute(key, value, indent="", single_attribute=True):
176313 if key .startswith ((":" , "@" , "v-" )) and isinstance (value , str ) and value .strip ():
177314 value = value .strip ()
178315 if key .startswith ("v-for" ):
179- formatted_value = format_list_expression (value )
316+ formatted_value = format_list_expression (value , expression_cache )
180317 return f'{ key } ="{ formatted_value } "'
181318 else :
182- formatted_value = format_python_expression (value )
319+ formatted_value = format_python_expression (value , expression_cache )
183320 formatted_value = textwrap .indent (
184321 formatted_value , indent + ("" if single_attribute else INDENT )
185322 ).lstrip ()
@@ -188,7 +325,7 @@ def format_attribute(key, value, indent="", single_attribute=True):
188325 return f'{ key } ="{ value } "'
189326
190327
191- def format_node (node , depth : int ) -> list [str ]:
328+ def format_node (node , depth : int , expression_cache = None ) -> list [str ]:
192329 result = []
193330 indent = depth * INDENT
194331
@@ -201,13 +338,23 @@ def format_node(node, depth: int) -> list[str]:
201338 if node .attrs :
202339 if len (node .attrs ) == 1 :
203340 key , val = next (iter (node .attrs .items ()))
204- attr = format_attribute (key , val , indent , single_attribute = True )
341+ attr = format_attribute (
342+ key ,
343+ val ,
344+ indent ,
345+ single_attribute = True ,
346+ expression_cache = expression_cache ,
347+ )
205348 start = f"{ start } { attr } "
206349 else :
207350 attrs = []
208351 for key in sorted (node .attrs , key = sort_attr ):
209352 attr = format_attribute (
210- key , node .attrs [key ], indent , single_attribute = False
353+ key ,
354+ node .attrs [key ],
355+ indent ,
356+ single_attribute = False ,
357+ expression_cache = expression_cache ,
211358 )
212359 attrs .append (f"{ indent } { INDENT } { attr } " )
213360
@@ -228,7 +375,7 @@ def format_node(node, depth: int) -> list[str]:
228375
229376 if hasattr (node , "children" ):
230377 for child in node .children :
231- result .extend (format_node (child , depth + 1 ))
378+ result .extend (format_node (child , depth + 1 , expression_cache ))
232379
233380 if node .children :
234381 result .append (f"{ indent } </{ node .tag } >" )
0 commit comments