11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- using System . Diagnostics ;
54using System . Globalization ;
65using Microsoft . AspNetCore . Components . Rendering ;
76
@@ -13,6 +12,9 @@ namespace Microsoft.AspNetCore.Components.Routing;
1312/// </summary>
1413public class NavLink : ComponentBase , IDisposable
1514{
15+ private const string DisableMatchAllIgnoresLeftUriPartSwitchKey = "Microsoft.AspNetCore.Components.Routing.NavLink.DisableMatchAllIgnoresLeftUriPart" ;
16+ private static readonly bool _disableMatchAllIgnoresLeftUriPart = AppContext . TryGetSwitch ( DisableMatchAllIgnoresLeftUriPartSwitchKey , out var switchValue ) && switchValue ;
17+
1618 private const string DefaultActiveClass = "active" ;
1719
1820 private bool _isActive ;
@@ -106,14 +108,21 @@ private void OnLocationChanged(object? sender, LocationChangedEventArgs args)
106108 }
107109 }
108110
109- private bool ShouldMatch ( string currentUriAbsolute )
111+ /// <summary>
112+ /// Determines whether the current URI should match the link.
113+ /// </summary>
114+ /// <param name="currentUriAbsolute">The absolute URI of the current location.</param>
115+ /// <returns>True if the link should be highlighted as active; otherwise, false.</returns>
116+ protected virtual bool ShouldMatch ( string currentUriAbsolute )
110117 {
111118 if ( _hrefAbsolute == null )
112119 {
113120 return false ;
114121 }
115122
116- if ( EqualsHrefExactlyOrIfTrailingSlashAdded ( currentUriAbsolute ) )
123+ var currentUriAbsoluteSpan = currentUriAbsolute . AsSpan ( ) ;
124+ var hrefAbsoluteSpan = _hrefAbsolute . AsSpan ( ) ;
125+ if ( EqualsHrefExactlyOrIfTrailingSlashAdded ( currentUriAbsoluteSpan , hrefAbsoluteSpan ) )
117126 {
118127 return true ;
119128 }
@@ -124,19 +133,62 @@ private bool ShouldMatch(string currentUriAbsolute)
124133 return true ;
125134 }
126135
127- return false ;
136+ if ( _disableMatchAllIgnoresLeftUriPart || Match != NavLinkMatch . All )
137+ {
138+ return false ;
139+ }
140+
141+ var uriWithoutQueryAndFragment = GetUriIgnoreQueryAndFragment ( currentUriAbsoluteSpan ) ;
142+ if ( EqualsHrefExactlyOrIfTrailingSlashAdded ( uriWithoutQueryAndFragment , hrefAbsoluteSpan ) )
143+ {
144+ return true ;
145+ }
146+ hrefAbsoluteSpan = GetUriIgnoreQueryAndFragment ( hrefAbsoluteSpan ) ;
147+ return EqualsHrefExactlyOrIfTrailingSlashAdded ( uriWithoutQueryAndFragment , hrefAbsoluteSpan ) ;
128148 }
129149
130- private bool EqualsHrefExactlyOrIfTrailingSlashAdded ( string currentUriAbsolute )
150+ private static ReadOnlySpan < char > GetUriIgnoreQueryAndFragment ( ReadOnlySpan < char > uri )
131151 {
132- Debug . Assert ( _hrefAbsolute != null ) ;
152+ if ( uri . IsEmpty )
153+ {
154+ return ReadOnlySpan < char > . Empty ;
155+ }
133156
134- if ( string . Equals ( currentUriAbsolute , _hrefAbsolute , StringComparison . OrdinalIgnoreCase ) )
157+ var queryStartPos = uri . IndexOf ( '?' ) ;
158+ var fragmentStartPos = uri . IndexOf ( '#' ) ;
159+
160+ if ( queryStartPos < 0 && fragmentStartPos < 0 )
161+ {
162+ return uri ;
163+ }
164+
165+ int minPos ;
166+ if ( queryStartPos < 0 )
167+ {
168+ minPos = fragmentStartPos ;
169+ }
170+ else if ( fragmentStartPos < 0 )
171+ {
172+ minPos = queryStartPos ;
173+ }
174+ else
175+ {
176+ minPos = Math . Min ( queryStartPos , fragmentStartPos ) ;
177+ }
178+
179+ return uri . Slice ( 0 , minPos ) ;
180+ }
181+
182+ private static readonly CaseInsensitiveCharComparer CaseInsensitiveComparer = new CaseInsensitiveCharComparer ( ) ;
183+
184+ private static bool EqualsHrefExactlyOrIfTrailingSlashAdded ( ReadOnlySpan < char > currentUriAbsolute , ReadOnlySpan < char > hrefAbsolute )
185+ {
186+ if ( currentUriAbsolute . SequenceEqual ( hrefAbsolute , CaseInsensitiveComparer ) )
135187 {
136188 return true ;
137189 }
138190
139- if ( currentUriAbsolute . Length == _hrefAbsolute . Length - 1 )
191+ if ( currentUriAbsolute . Length == hrefAbsolute . Length - 1 )
140192 {
141193 // Special case: highlight links to http://host/path/ even if you're
142194 // at http://host/path (with no trailing slash)
@@ -146,8 +198,8 @@ private bool EqualsHrefExactlyOrIfTrailingSlashAdded(string currentUriAbsolute)
146198 // which in turn is because it's common for servers to return the same page
147199 // for http://host/vdir as they do for host://host/vdir/ as it's no
148200 // good to display a blank page in that case.
149- if ( _hrefAbsolute [ _hrefAbsolute . Length - 1 ] == '/'
150- && _hrefAbsolute . StartsWith ( currentUriAbsolute , StringComparison . OrdinalIgnoreCase ) )
201+ if ( hrefAbsolute [ hrefAbsolute . Length - 1 ] == '/' &&
202+ currentUriAbsolute . SequenceEqual ( hrefAbsolute . Slice ( 0 , hrefAbsolute . Length - 1 ) , CaseInsensitiveComparer ) )
151203 {
152204 return true ;
153205 }
@@ -199,7 +251,7 @@ private static bool IsStrictlyPrefixWithSeparator(string value, string prefix)
199251
200252 private static bool IsUnreservedCharacter ( char c )
201253 {
202- // Checks whether it is an unreserved character according to
254+ // Checks whether it is an unreserved character according to
203255 // https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
204256 // Those are characters that are allowed in a URI but do not have a reserved
205257 // purpose (e.g. they do not separate the components of the URI)
@@ -209,4 +261,17 @@ private static bool IsUnreservedCharacter(char c)
209261 c == '_' ||
210262 c == '~' ;
211263 }
264+
265+ private class CaseInsensitiveCharComparer : IEqualityComparer < char >
266+ {
267+ public bool Equals ( char x , char y )
268+ {
269+ return char . ToLowerInvariant ( x ) == char . ToLowerInvariant ( y ) ;
270+ }
271+
272+ public int GetHashCode ( char obj )
273+ {
274+ return char . ToLowerInvariant ( obj ) . GetHashCode ( ) ;
275+ }
276+ }
212277}
0 commit comments