@@ -127,23 +127,194 @@ def validate_fix(self, fix: Fix, file_content: str) -> bool:
127127 if not line_content :
128128 return False
129129
130- # Check if we're inside a code block
131- # Look for code block delimiters before the current line
132- lines = file_content .splitlines ()
133- in_code_block = False
134- for i in range (fix .violation .line - 1 ):
135- line = lines [i ].strip ()
136- if line == "----" or line == "...." :
137- in_code_block = not in_code_block
138-
139- if in_code_block :
140- return False
141-
142130 # Check if we're inside inline code
143131 # Count backticks before the entity position
144132 before_text = line_content [:fix .violation .column - 1 ]
145133 backtick_count = before_text .count ("`" )
146134 if backtick_count % 2 != 0 : # Odd number means we're inside inline code
147135 return False
148136
149- return True
137+ # Check if we're inside a code block and if replacements are enabled
138+ lines = file_content .splitlines ()
139+ code_block_info = self ._get_code_block_context (lines , fix .violation .line - 1 )
140+
141+ if code_block_info ['in_code_block' ]:
142+ # Check if replacements are enabled for this code block
143+ if code_block_info ['replacements_enabled' ]:
144+ return True # Entities should be processed
145+ else :
146+ return False # Entities should remain literal
147+
148+ return True
149+
150+ def _get_code_block_context (self , lines : list , target_line_idx : int ) -> dict :
151+ """Determine if we're in a code block and check its substitution settings.
152+
153+ Args:
154+ lines: List of all lines in the document
155+ target_line_idx: Zero-based index of the target line
156+
157+ Returns:
158+ Dict with 'in_code_block' and 'replacements_enabled' flags
159+ """
160+ in_code_block = False
161+ block_type = None
162+ block_start_line = - 1
163+ subs_value = None
164+ pending_source_subs = None # Store subs from [source] line
165+
166+ for i in range (min (target_line_idx + 1 , len (lines ))):
167+ line = lines [i ].strip ()
168+
169+ # First check if this is a source attribute line
170+ if line .startswith ("[source" ):
171+ # Extract subs but don't mark as in block yet
172+ pending_source_subs = self ._extract_subs_from_line (line )
173+ continue
174+
175+ # Check for listing/source block delimiters
176+ if line == "----" :
177+ if not in_code_block :
178+ in_code_block = True
179+ block_type = "listing"
180+ block_start_line = i
181+ # Check if there was a source line just before
182+ if i > 0 and pending_source_subs is not None :
183+ subs_value = pending_source_subs
184+ block_type = "source"
185+ else :
186+ # Look for other attributes in previous lines
187+ subs_value = self ._find_block_attributes (lines , i )
188+ pending_source_subs = None # Reset
189+ else :
190+ # Closing delimiter
191+ in_code_block = False
192+ block_type = None
193+ subs_value = None
194+ pending_source_subs = None
195+ elif line == "...." :
196+ if not in_code_block :
197+ in_code_block = True
198+ block_type = "literal"
199+ block_start_line = i
200+ # Look for attributes in previous lines
201+ subs_value = self ._find_block_attributes (lines , i )
202+ else :
203+ # Closing delimiter
204+ in_code_block = False
205+ block_type = None
206+ subs_value = None
207+ else :
208+ # Any other line resets pending source
209+ if line and not line .startswith ("[" ):
210+ pending_source_subs = None
211+
212+ # Determine if replacements are enabled
213+ replacements_enabled = False
214+ if subs_value :
215+ # Parse subs value
216+ subs_list = self ._parse_subs_value (subs_value )
217+ replacements_enabled = 'replacements' in subs_list
218+
219+ return {
220+ 'in_code_block' : in_code_block ,
221+ 'replacements_enabled' : replacements_enabled ,
222+ 'block_type' : block_type ,
223+ 'subs' : subs_value
224+ }
225+
226+ def _find_block_attributes (self , lines : list , delimiter_idx : int ) -> Optional [str ]:
227+ """Find block attributes that might contain subs setting.
228+
229+ Args:
230+ lines: List of all lines
231+ delimiter_idx: Index of the block delimiter line
232+
233+ Returns:
234+ The subs value if found, None otherwise
235+ """
236+ # Look backwards for block attributes (up to 3 lines)
237+ for i in range (max (0 , delimiter_idx - 3 ), delimiter_idx ):
238+ line = lines [i ].strip ()
239+ # Check for [source,...] or [listing,...] style attributes
240+ if line .startswith ('[' ) and line .endswith (']' ):
241+ return self ._extract_subs_from_line (line )
242+ return None
243+
244+ def _extract_subs_from_line (self , line : str ) -> Optional [str ]:
245+ """Extract subs value from an attribute line.
246+
247+ Args:
248+ line: Line containing attributes like [source,java,subs="attributes+"]
249+
250+ Returns:
251+ The subs value if found, None otherwise
252+ """
253+ import re
254+
255+ # Look for subs="value" or subs='value'
256+ match = re .search (r'subs\s*=\s*["\']([^"\']+)["\']' , line )
257+ if match :
258+ return match .group (1 )
259+
260+ # Look for subs=value (without quotes)
261+ match = re .search (r'subs\s*=\s*([^,\]]+)' , line )
262+ if match :
263+ return match .group (1 ).strip ()
264+
265+ return None
266+
267+ def _parse_subs_value (self , subs_value : str ) -> list :
268+ """Parse the subs attribute value into a list of substitutions.
269+
270+ Args:
271+ subs_value: Value like "attributes+", "replacements", "+replacements,-attributes"
272+
273+ Returns:
274+ List of active substitution types
275+ """
276+ if not subs_value :
277+ return []
278+
279+ # Handle special values
280+ if subs_value == 'normal' :
281+ # Normal substitutions
282+ return ['specialcharacters' , 'quotes' , 'attributes' , 'replacements' , 'macros' , 'post_replacements' ]
283+ elif subs_value == 'none' :
284+ return []
285+ elif subs_value == 'verbatim' :
286+ return ['specialcharacters' ]
287+
288+ # For code blocks, default is no substitutions
289+ # We start with empty list and only add what's explicitly requested
290+ active_subs = []
291+
292+ # Parse comma-separated list with +/- modifiers
293+ parts = [p .strip () for p in subs_value .split (',' )]
294+
295+ for part in parts :
296+ if not part :
297+ continue
298+
299+ # Check for trailing + which means "add to defaults"
300+ # For code blocks, default is empty, so "attributes+" just adds attributes
301+ if part .endswith ('+' ) and not part .startswith ('+' ):
302+ sub_type = part [:- 1 ] # Remove trailing +
303+ if sub_type and sub_type not in active_subs :
304+ active_subs .append (sub_type )
305+ elif part .startswith ('+' ):
306+ # Explicit add with +prefix
307+ sub_type = part [1 :]
308+ if sub_type and sub_type not in active_subs :
309+ active_subs .append (sub_type )
310+ elif part .startswith ('-' ):
311+ # Remove from existing
312+ sub_type = part [1 :]
313+ if sub_type in active_subs :
314+ active_subs .remove (sub_type )
315+ else :
316+ # No modifier - this replaces everything
317+ if part not in active_subs :
318+ active_subs .append (part )
319+
320+ return active_subs
0 commit comments