@@ -90,6 +90,9 @@ export default class extends Controller {
9090 "formatTooltip" ,
9191 "autofixButton" ,
9292 "autofixTooltip" ,
93+ "autofixUnsafeWrapper" ,
94+ "autofixUnsafeButton" ,
95+ "autofixUnsafeTooltip" ,
9396 "printerViewer" ,
9497 "printerOutput" ,
9598 "printerVerification" ,
@@ -204,6 +207,7 @@ export default class extends Controller {
204207 this . setupThemeListener ( )
205208 this . setupTooltip ( )
206209 this . setupAutofixTooltip ( )
210+ this . setupAutofixUnsafeTooltip ( )
207211 this . setupShareTooltip ( )
208212 this . setupGitHubTooltip ( )
209213 this . setupCopyTooltip ( )
@@ -265,6 +269,7 @@ export default class extends Controller {
265269 window . removeEventListener ( "popstate" , this . handlePopState )
266270 this . removeTooltip ( )
267271 this . removeAutofixTooltip ( )
272+ this . removeAutofixUnsafeTooltip ( )
268273 this . removeShareTooltip ( )
269274 this . removeGitHubTooltip ( )
270275 this . removeCopyTooltip ( )
@@ -785,6 +790,60 @@ export default class extends Controller {
785790 }
786791 }
787792
793+ async autofixUnsafeEditor ( event ) {
794+ if ( this . isRubyMode ) return
795+
796+ const button = this . getClosestButton ( event . target )
797+
798+ if ( button . disabled ) {
799+ return
800+ }
801+
802+ const warningIcon = button . querySelector ( ".fa-triangle-exclamation" )
803+ const checkIcon = button . querySelector ( ".fa-circle-check" )
804+
805+ try {
806+ const value = this . editor ? this . editor . getValue ( ) : this . inputTarget . value
807+ const linter = new Linter ( Herb )
808+ const result = linter . autofix ( value , undefined , undefined , { includeUnsafe : true } )
809+
810+ if ( result && typeof result === "object" && "source" in result ) {
811+ const fixedCount = Array . isArray ( result . fixed ) ? result . fixed . length : 0
812+
813+ if ( fixedCount > 0 && typeof result . source === "string" ) {
814+ if ( this . editor ) {
815+ this . editor . setValue ( result . source )
816+ } else {
817+ this . inputTarget . value = result . source
818+ }
819+
820+ if ( warningIcon && checkIcon ) {
821+ warningIcon . classList . add ( "hidden" )
822+ checkIcon . classList . remove ( "hidden" )
823+ checkIcon . style . display = ""
824+
825+ setTimeout ( ( ) => {
826+ this . resetAutofixUnsafeButtonIcons ( )
827+ } , 1000 )
828+ }
829+
830+ const offensesLabel = fixedCount === 1 ? "offense" : "offenses"
831+ this . showTemporaryMessage ( `Autofixed ${ fixedCount } unsafe linter ${ offensesLabel } ` , "success" )
832+
833+ await this . analyze ( )
834+ this . resetAutofixUnsafeButtonIcons ( )
835+ } else {
836+ this . showTemporaryMessage ( "No unsafe autocorrectable linter offenses found" , "info" )
837+ }
838+ } else {
839+ this . showTemporaryMessage ( "Failed to autofix unsafe linter offenses" , "error" )
840+ }
841+ } catch ( error ) {
842+ console . error ( "Autofix unsafe error:" , error )
843+ this . showTemporaryMessage ( "Failed to autofix unsafe linter offenses" , "error" )
844+ }
845+ }
846+
788847 async analyze ( ) {
789848 this . updateURL ( )
790849
@@ -1036,6 +1095,20 @@ export default class extends Controller {
10361095 }
10371096 }
10381097
1098+ if ( this . hasAutofixUnsafeWrapperTarget ) {
1099+ const hasParserErrors = result . parseResult ? result . parseResult . recursiveErrors ( ) . length > 0 : false
1100+ const hasUnsafeOffenses = ! ! ( result . lintResult && Array . isArray ( result . lintResult . offenses ) &&
1101+ result . lintResult . offenses . some ( offense => offense . autofixContext && offense . autofixContext . unsafe === true ) )
1102+
1103+ if ( hasParserErrors || ! hasUnsafeOffenses ) {
1104+ this . autofixUnsafeWrapperTarget . classList . add ( 'hidden' )
1105+ } else {
1106+ this . autofixUnsafeWrapperTarget . classList . remove ( 'hidden' )
1107+ this . enableAutofixUnsafeButton ( )
1108+ this . updateAutofixUnsafeTooltipText ( 'Autocorrect unsafe Herb Linter offenses' )
1109+ }
1110+ }
1111+
10391112 if ( this . hasRubyViewerTarget ) {
10401113 this . rubyViewerTarget . classList . add ( "language-ruby" )
10411114 this . rubyViewerTarget . textContent = result . ruby
@@ -1717,6 +1790,72 @@ export default class extends Controller {
17171790 }
17181791 }
17191792
1793+ setupAutofixUnsafeTooltip ( ) {
1794+ if ( this . hasAutofixUnsafeTooltipTarget ) {
1795+ this . autofixUnsafeButtonTarget . addEventListener ( 'mouseenter' , this . showAutofixUnsafeTooltip )
1796+ this . autofixUnsafeButtonTarget . addEventListener ( 'mouseleave' , this . hideAutofixUnsafeTooltip )
1797+ }
1798+ }
1799+
1800+ removeAutofixUnsafeTooltip ( ) {
1801+ if ( this . hasAutofixUnsafeTooltipTarget ) {
1802+ this . autofixUnsafeButtonTarget . removeEventListener ( 'mouseenter' , this . showAutofixUnsafeTooltip )
1803+ this . autofixUnsafeButtonTarget . removeEventListener ( 'mouseleave' , this . hideAutofixUnsafeTooltip )
1804+
1805+ this . hideAutofixUnsafeTooltip ( )
1806+ }
1807+ }
1808+
1809+ showAutofixUnsafeTooltip = ( ) => {
1810+ if ( this . hasAutofixUnsafeTooltipTarget ) {
1811+ this . autofixUnsafeTooltipTarget . classList . remove ( 'hidden' )
1812+ }
1813+ }
1814+
1815+ hideAutofixUnsafeTooltip = ( ) => {
1816+ if ( this . hasAutofixUnsafeTooltipTarget ) {
1817+ this . autofixUnsafeTooltipTarget . classList . add ( 'hidden' )
1818+ }
1819+ }
1820+
1821+ updateAutofixUnsafeTooltipText ( text ) {
1822+ if ( this . hasAutofixUnsafeTooltipTarget ) {
1823+ const textNode = this . autofixUnsafeTooltipTarget . firstChild
1824+ if ( textNode && textNode . nodeType === Node . TEXT_NODE ) {
1825+ textNode . textContent = text
1826+ }
1827+ }
1828+ }
1829+
1830+ enableAutofixUnsafeButton ( ) {
1831+ this . autofixUnsafeButtonTarget . disabled = false
1832+ this . autofixUnsafeButtonTarget . classList . remove ( 'opacity-50' , 'cursor-not-allowed' )
1833+ this . autofixUnsafeButtonTarget . classList . add ( 'hover:bg-gray-200' , 'dark:hover:bg-gray-700' )
1834+ }
1835+
1836+ disableAutofixUnsafeButton ( ) {
1837+ this . autofixUnsafeButtonTarget . disabled = true
1838+ this . autofixUnsafeButtonTarget . classList . add ( 'opacity-50' , 'cursor-not-allowed' )
1839+ this . autofixUnsafeButtonTarget . classList . remove ( 'hover:bg-gray-200' , 'dark:hover:bg-gray-700' )
1840+ this . resetAutofixUnsafeButtonIcons ( )
1841+ }
1842+
1843+ resetAutofixUnsafeButtonIcons ( ) {
1844+ if ( ! this . hasAutofixUnsafeButtonTarget ) return
1845+
1846+ const warningIcon = this . autofixUnsafeButtonTarget . querySelector ( ".fa-triangle-exclamation" )
1847+ const checkIcon = this . autofixUnsafeButtonTarget . querySelector ( ".fa-circle-check" )
1848+
1849+ if ( warningIcon ) {
1850+ warningIcon . classList . remove ( "hidden" )
1851+ }
1852+
1853+ if ( checkIcon ) {
1854+ checkIcon . classList . add ( "hidden" )
1855+ checkIcon . style . display = ""
1856+ }
1857+ }
1858+
17201859 setupShareTooltip ( ) {
17211860 if ( this . hasShareButtonTarget && this . hasShareTooltipTarget ) {
17221861 this . shareButtonTarget . addEventListener ( 'mouseenter' , this . showShareTooltip )
0 commit comments