@@ -1253,18 +1253,30 @@ function createAttrNoUnnecessaryWhitespaceFix(
12531253}
12541254
12551255/**
1256- * Create auto-fix action for spec-char-escape rule
1256+ * Create auto-fix action for attr-whitespace rule
1257+ *
1258+ * This fixes attribute values that have leading or trailing whitespace.
1259+ * The fix removes leading and trailing spaces from attribute values.
1260+ *
1261+ * Example:
1262+ * - Before: <div title=" a "></div>
1263+ * - After: <div title="a"></div>
12571264 */
1258- function createSpecCharEscapeFix (
1265+ function createAttrWhitespaceFix (
12591266 document : TextDocument ,
12601267 diagnostic : Diagnostic ,
12611268) : CodeAction | null {
1269+ trace (
1270+ `[DEBUG] createAttrWhitespaceFix called with diagnostic: ${ JSON . stringify ( diagnostic ) } ` ,
1271+ ) ;
1272+
12621273 if (
12631274 ! diagnostic . data ||
1264- diagnostic . data . ruleId !== "spec-char-escape " ||
1275+ diagnostic . data . ruleId !== "attr-whitespace " ||
12651276 typeof diagnostic . data . line !== "number" ||
12661277 typeof diagnostic . data . col !== "number"
12671278 ) {
1279+ trace ( `[DEBUG] createAttrWhitespaceFix: Invalid diagnostic data or ruleId` ) ;
12681280 return null ;
12691281 }
12701282
@@ -1273,55 +1285,48 @@ function createSpecCharEscapeFix(
12731285 const line = lines [ diagnostic . data . line - 1 ] ;
12741286
12751287 if ( ! line ) {
1288+ trace (
1289+ `[DEBUG] createAttrWhitespaceFix: No line found at ${ diagnostic . data . line } ` ,
1290+ ) ;
12761291 return null ;
12771292 }
12781293
1279- // Find unescaped special characters that need to be escaped
1280- // We need to be careful not to escape characters that are already in HTML tags or attributes
1281- const specialCharPattern = / ( [ < > ] ) / g;
1294+ // Find attributes with leading or trailing whitespace in their values
1295+ // This pattern matches: attrName=" value " or attrName=' value '
1296+ const attrPattern = / ( [ a - z A - Z 0 - 9 - _ ] + ) \s * = \s * ( " ( [ ^ " ] * ) " | ' ( [ ^ ' ] * ) ' ) / g;
12821297 let match ;
12831298 const edits : TextEdit [ ] = [ ] ;
12841299
1285- while ( ( match = specialCharPattern . exec ( line ) ) !== null ) {
1300+ while ( ( match = attrPattern . exec ( line ) ) !== null ) {
12861301 const startCol = match . index ;
1287- const endCol = startCol + match [ 1 ] . length ;
1288- const char = match [ 1 ] ;
1302+ const endCol = startCol + match [ 0 ] . length ;
1303+ const attrName = match [ 1 ] ;
1304+ const quoteType = match [ 2 ] . startsWith ( '"' ) ? '"' : "'" ;
1305+ const attrValue = match [ 3 ] || match [ 4 ] ; // match[3] for double quotes, match[4] for single quotes
12891306
12901307 // Check if this match is at or near the diagnostic position
12911308 const diagnosticCol = diagnostic . data . col - 1 ;
1292- if ( Math . abs ( startCol - diagnosticCol ) <= 5 ) {
1293- // Determine if this character is inside a tag (should not be escaped)
1294- const beforeMatch = line . substring ( 0 , startCol ) ;
1295- const lastOpenBracket = beforeMatch . lastIndexOf ( "<" ) ;
1296- const lastCloseBracket = beforeMatch . lastIndexOf ( ">" ) ;
1297-
1298- // If we're inside a tag (after < but before >), don't escape
1299- if ( lastOpenBracket > lastCloseBracket ) {
1300- continue ;
1301- }
1302-
1303- const lineIndex = diagnostic . data . line - 1 ;
1304- const startPos = { line : lineIndex , character : startCol } ;
1305- const endPos = { line : lineIndex , character : endCol } ;
1306-
1307- // Map characters to their HTML entities
1308- const entityMap : { [ key : string ] : string } = {
1309- "<" : "<" ,
1310- ">" : ">" ,
1311- } ;
1309+ if ( Math . abs ( startCol - diagnosticCol ) <= 10 ) {
1310+ // Check if there's leading or trailing whitespace
1311+ const trimmedValue = attrValue . trim ( ) ;
1312+ if ( trimmedValue !== attrValue ) {
1313+ const startPos = {
1314+ line : diagnostic . data . line - 1 ,
1315+ character : startCol ,
1316+ } ;
1317+ const endPos = { line : diagnostic . data . line - 1 , character : endCol } ;
13121318
1313- const replacement = entityMap [ char ] ;
1314- if ( replacement ) {
13151319 edits . push ( {
13161320 range : { start : startPos , end : endPos } ,
1317- newText : replacement ,
1321+ newText : ` ${ attrName } = ${ quoteType } ${ trimmedValue } ${ quoteType } ` ,
13181322 } ) ;
13191323 break ; // Only fix the first occurrence near the diagnostic
13201324 }
13211325 }
13221326 }
13231327
13241328 if ( edits . length === 0 ) {
1329+ trace ( `[DEBUG] createAttrWhitespaceFix: No edits created` ) ;
13251330 return null ;
13261331 }
13271332
@@ -1332,7 +1337,7 @@ function createSpecCharEscapeFix(
13321337 } ;
13331338
13341339 return {
1335- title : "Escape special character " ,
1340+ title : "Remove leading/trailing whitespace from attribute value " ,
13361341 kind : CodeActionKind . QuickFix ,
13371342 edit : workspaceEdit ,
13381343 isPreferred : true ,
@@ -1466,6 +1471,93 @@ function createTagSelfCloseFix(
14661471 return action ;
14671472}
14681473
1474+ /**
1475+ * Create auto-fix action for spec-char-escape rule
1476+ */
1477+ function createSpecCharEscapeFix (
1478+ document : TextDocument ,
1479+ diagnostic : Diagnostic ,
1480+ ) : CodeAction | null {
1481+ if (
1482+ ! diagnostic . data ||
1483+ diagnostic . data . ruleId !== "spec-char-escape" ||
1484+ typeof diagnostic . data . line !== "number" ||
1485+ typeof diagnostic . data . col !== "number"
1486+ ) {
1487+ return null ;
1488+ }
1489+
1490+ const text = document . getText ( ) ;
1491+ const lines = text . split ( "\n" ) ;
1492+ const line = lines [ diagnostic . data . line - 1 ] ;
1493+
1494+ if ( ! line ) {
1495+ return null ;
1496+ }
1497+
1498+ // Find unescaped special characters that need to be escaped
1499+ // We need to be careful not to escape characters that are already in HTML tags or attributes
1500+ const specialCharPattern = / ( [ < > ] ) / g;
1501+ let match ;
1502+ const edits : TextEdit [ ] = [ ] ;
1503+
1504+ while ( ( match = specialCharPattern . exec ( line ) ) !== null ) {
1505+ const startCol = match . index ;
1506+ const endCol = startCol + match [ 1 ] . length ;
1507+ const char = match [ 1 ] ;
1508+
1509+ // Check if this match is at or near the diagnostic position
1510+ const diagnosticCol = diagnostic . data . col - 1 ;
1511+ if ( Math . abs ( startCol - diagnosticCol ) <= 5 ) {
1512+ // Determine if this character is inside a tag (should not be escaped)
1513+ const beforeMatch = line . substring ( 0 , startCol ) ;
1514+ const lastOpenBracket = beforeMatch . lastIndexOf ( "<" ) ;
1515+ const lastCloseBracket = beforeMatch . lastIndexOf ( ">" ) ;
1516+
1517+ // If we're inside a tag (after < but before >), don't escape
1518+ if ( lastOpenBracket > lastCloseBracket ) {
1519+ continue ;
1520+ }
1521+
1522+ const lineIndex = diagnostic . data . line - 1 ;
1523+ const startPos = { line : lineIndex , character : startCol } ;
1524+ const endPos = { line : lineIndex , character : endCol } ;
1525+
1526+ // Map characters to their HTML entities
1527+ const entityMap : { [ key : string ] : string } = {
1528+ "<" : "<" ,
1529+ ">" : ">" ,
1530+ } ;
1531+
1532+ const replacement = entityMap [ char ] ;
1533+ if ( replacement ) {
1534+ edits . push ( {
1535+ range : { start : startPos , end : endPos } ,
1536+ newText : replacement ,
1537+ } ) ;
1538+ break ; // Only fix the first occurrence near the diagnostic
1539+ }
1540+ }
1541+ }
1542+
1543+ if ( edits . length === 0 ) {
1544+ return null ;
1545+ }
1546+
1547+ const workspaceEdit : WorkspaceEdit = {
1548+ changes : {
1549+ [ document . uri ] : edits ,
1550+ } ,
1551+ } ;
1552+
1553+ return {
1554+ title : "Escape special character" ,
1555+ kind : CodeActionKind . QuickFix ,
1556+ edit : workspaceEdit ,
1557+ isPreferred : true ,
1558+ } ;
1559+ }
1560+
14691561/**
14701562 * Create auto-fix actions for supported rules
14711563 */
@@ -1544,6 +1636,10 @@ async function createAutoFixes(
15441636 trace ( `[DEBUG] Calling createAttrNoUnnecessaryWhitespaceFix` ) ;
15451637 fix = createAttrNoUnnecessaryWhitespaceFix ( document , diagnostic ) ;
15461638 break ;
1639+ case "attr-whitespace" :
1640+ trace ( `[DEBUG] Calling createAttrWhitespaceFix` ) ;
1641+ fix = createAttrWhitespaceFix ( document , diagnostic ) ;
1642+ break ;
15471643 case "spec-char-escape" :
15481644 trace ( `[DEBUG] Calling createSpecCharEscapeFix` ) ;
15491645 fix = createSpecCharEscapeFix ( document , diagnostic ) ;
0 commit comments