@@ -1566,6 +1566,226 @@ function createSpecCharEscapeFix(
15661566 } ;
15671567}
15681568
1569+ /**
1570+ * Create auto-fix action for tag-no-obsolete rule
1571+ *
1572+ * This fixes obsolete HTML tags by converting them to their modern equivalents.
1573+ * Currently supports converting <acronym> tags to <abbr> tags.
1574+ *
1575+ * Example:
1576+ * - Before: <acronym title="HyperText Markup Language">HTML</acronym>
1577+ * - After: <abbr title="HyperText Markup Language">HTML</abbr>
1578+ */
1579+ function createTagNoObsoleteFix (
1580+ document : TextDocument ,
1581+ diagnostic : Diagnostic ,
1582+ ) : CodeAction | null {
1583+ trace (
1584+ `[DEBUG] createTagNoObsoleteFix called with diagnostic: ${ JSON . stringify ( diagnostic ) } ` ,
1585+ ) ;
1586+
1587+ if ( ! diagnostic . data || diagnostic . data . ruleId !== "tag-no-obsolete" ) {
1588+ trace ( `[DEBUG] createTagNoObsoleteFix: Invalid diagnostic data or ruleId` ) ;
1589+ return null ;
1590+ }
1591+
1592+ const text = document . getText ( ) ;
1593+ const edits : TextEdit [ ] = [ ] ;
1594+
1595+ // Find all acronym tags in the entire document
1596+ const acronymPattern = / < \/ ? ( a c r o n y m | A C R O N Y M ) ( (?: \s + [ ^ > ] * ?) ? ) \s * ( \/ ? ) > / gi;
1597+ let match ;
1598+ const acronymTags : Array < {
1599+ startOffset : number ;
1600+ endOffset : number ;
1601+ tagName : string ;
1602+ attributes : string ;
1603+ selfClosing : string ;
1604+ isClosingTag : boolean ;
1605+ fullMatch : string ;
1606+ line : number ;
1607+ column : number ;
1608+ } > = [ ] ;
1609+
1610+ // Collect all acronym tags with their positions
1611+ while ( ( match = acronymPattern . exec ( text ) ) !== null ) {
1612+ const startOffset = match . index ;
1613+ const endOffset = startOffset + match [ 0 ] . length ;
1614+ const tagName = match [ 1 ] . toLowerCase ( ) ;
1615+ const attributes = match [ 2 ] || "" ;
1616+ const selfClosing = match [ 3 ] || "" ;
1617+ const isClosingTag = match [ 0 ] . startsWith ( "</" ) ;
1618+
1619+ // Only handle acronym tags for now
1620+ if ( tagName === "acronym" ) {
1621+ const position = document . positionAt ( startOffset ) ;
1622+ acronymTags . push ( {
1623+ startOffset,
1624+ endOffset,
1625+ tagName,
1626+ attributes,
1627+ selfClosing,
1628+ isClosingTag,
1629+ fullMatch : match [ 0 ] ,
1630+ line : position . line ,
1631+ column : position . character ,
1632+ } ) ;
1633+ }
1634+ }
1635+
1636+ // Find the diagnostic position
1637+ const diagnosticLine = diagnostic . data . line - 1 ;
1638+ const diagnosticCol = diagnostic . data . col - 1 ;
1639+
1640+ // Find the closest acronym tag to the diagnostic position
1641+ let closestTag : ( typeof acronymTags ) [ 0 ] | null = null ;
1642+ let minDistance = Infinity ;
1643+
1644+ for ( const tag of acronymTags ) {
1645+ const distance =
1646+ Math . abs ( tag . line - diagnosticLine ) +
1647+ Math . abs ( tag . column - diagnosticCol ) ;
1648+ if ( distance < minDistance ) {
1649+ minDistance = distance ;
1650+ closestTag = tag ;
1651+ }
1652+ }
1653+
1654+ if ( ! closestTag || minDistance > 20 ) {
1655+ trace ( `[DEBUG] createTagNoObsoleteFix: No close acronym tag found` ) ;
1656+ return null ;
1657+ }
1658+
1659+ // If this is a self-closing tag, just convert it
1660+ if ( closestTag . selfClosing === "/" ) {
1661+ const startPos = document . positionAt ( closestTag . startOffset ) ;
1662+ const endPos = document . positionAt ( closestTag . endOffset ) ;
1663+
1664+ edits . push ( {
1665+ range : { start : startPos , end : endPos } ,
1666+ newText : `<abbr${ closestTag . attributes } />` ,
1667+ } ) ;
1668+ } else if ( closestTag . isClosingTag ) {
1669+ // If this is a closing tag, find its opening tag
1670+ const openingTag = findOpeningTag ( acronymTags , closestTag ) ;
1671+ if ( openingTag ) {
1672+ // Convert both opening and closing tags
1673+ const openingStartPos = document . positionAt ( openingTag . startOffset ) ;
1674+ const openingEndPos = document . positionAt ( openingTag . endOffset ) ;
1675+ const closingStartPos = document . positionAt ( closestTag . startOffset ) ;
1676+ const closingEndPos = document . positionAt ( closestTag . endOffset ) ;
1677+
1678+ edits . push ( {
1679+ range : { start : openingStartPos , end : openingEndPos } ,
1680+ newText : `<abbr${ openingTag . attributes } >` ,
1681+ } ) ;
1682+ edits . push ( {
1683+ range : { start : closingStartPos , end : closingEndPos } ,
1684+ newText : "</abbr>" ,
1685+ } ) ;
1686+ }
1687+ } else {
1688+ // If this is an opening tag, find its closing tag
1689+ const closingTag = findClosingTag ( acronymTags , closestTag ) ;
1690+ if ( closingTag ) {
1691+ // Convert both opening and closing tags
1692+ const openingStartPos = document . positionAt ( closestTag . startOffset ) ;
1693+ const openingEndPos = document . positionAt ( closestTag . endOffset ) ;
1694+ const closingStartPos = document . positionAt ( closingTag . startOffset ) ;
1695+ const closingEndPos = document . positionAt ( closingTag . endOffset ) ;
1696+
1697+ edits . push ( {
1698+ range : { start : openingStartPos , end : openingEndPos } ,
1699+ newText : `<abbr${ closestTag . attributes } >` ,
1700+ } ) ;
1701+ edits . push ( {
1702+ range : { start : closingStartPos , end : closingEndPos } ,
1703+ newText : "</abbr>" ,
1704+ } ) ;
1705+ }
1706+ }
1707+
1708+ if ( edits . length === 0 ) {
1709+ trace ( `[DEBUG] createTagNoObsoleteFix: No edits created` ) ;
1710+ return null ;
1711+ }
1712+
1713+ const workspaceEdit : WorkspaceEdit = {
1714+ changes : {
1715+ [ document . uri ] : edits ,
1716+ } ,
1717+ } ;
1718+
1719+ return {
1720+ title : "Convert obsolete tag to modern equivalent" ,
1721+ kind : CodeActionKind . QuickFix ,
1722+ edit : workspaceEdit ,
1723+ isPreferred : true ,
1724+ } ;
1725+ }
1726+
1727+ /**
1728+ * Helper function to find the opening tag for a given closing tag
1729+ */
1730+ function findOpeningTag (
1731+ tags : Array < {
1732+ startOffset : number ;
1733+ endOffset : number ;
1734+ tagName : string ;
1735+ attributes : string ;
1736+ selfClosing : string ;
1737+ isClosingTag : boolean ;
1738+ fullMatch : string ;
1739+ line : number ;
1740+ column : number ;
1741+ } > ,
1742+ closingTag : ( typeof tags ) [ 0 ] ,
1743+ ) : ( typeof tags ) [ 0 ] | null {
1744+ // Find the most recent opening tag before this closing tag
1745+ let openingTag : ( typeof tags ) [ 0 ] | null = null ;
1746+
1747+ for ( const tag of tags ) {
1748+ if ( ! tag . isClosingTag && tag . startOffset < closingTag . startOffset ) {
1749+ if ( ! openingTag || tag . startOffset > openingTag . startOffset ) {
1750+ openingTag = tag ;
1751+ }
1752+ }
1753+ }
1754+
1755+ return openingTag ;
1756+ }
1757+
1758+ /**
1759+ * Helper function to find the closing tag for a given opening tag
1760+ */
1761+ function findClosingTag (
1762+ tags : Array < {
1763+ startOffset : number ;
1764+ endOffset : number ;
1765+ tagName : string ;
1766+ attributes : string ;
1767+ selfClosing : string ;
1768+ isClosingTag : boolean ;
1769+ fullMatch : string ;
1770+ line : number ;
1771+ column : number ;
1772+ } > ,
1773+ openingTag : ( typeof tags ) [ 0 ] ,
1774+ ) : ( typeof tags ) [ 0 ] | null {
1775+ // Find the first closing tag after this opening tag
1776+ let closingTag : ( typeof tags ) [ 0 ] | null = null ;
1777+
1778+ for ( const tag of tags ) {
1779+ if ( tag . isClosingTag && tag . startOffset > openingTag . startOffset ) {
1780+ if ( ! closingTag || tag . startOffset < closingTag . startOffset ) {
1781+ closingTag = tag ;
1782+ }
1783+ }
1784+ }
1785+
1786+ return closingTag ;
1787+ }
1788+
15691789/**
15701790 * Create auto-fix actions for supported rules
15711791 */
@@ -1656,6 +1876,10 @@ async function createAutoFixes(
16561876 trace ( `[DEBUG] Calling createTagSelfCloseFix` ) ;
16571877 fix = createTagSelfCloseFix ( document , diagnostic ) ;
16581878 break ;
1879+ case "tag-no-obsolete" :
1880+ trace ( `[DEBUG] Calling createTagNoObsoleteFix` ) ;
1881+ fix = createTagNoObsoleteFix ( document , diagnostic ) ;
1882+ break ;
16591883 default :
16601884 trace ( `[DEBUG] No autofix function found for rule: ${ ruleId } ` ) ;
16611885 break ;
0 commit comments