diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs
index 2663e636bd7..f09b4c8176e 100644
--- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs
+++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ArrayExtensions.cs
@@ -1,13 +1,188 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
+using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+#if !NET
+using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper;
+#endif
+
namespace Microsoft.AspNetCore.Razor;
internal static class ArrayExtensions
{
+ ///
+ /// Creates a new span over the portion of the target array defined by an value.
+ ///
+ ///
+ /// The array to convert.
+ ///
+ ///
+ /// The starting index.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ ///
+ /// is covariant, and the array's type is not exactly [].
+ ///
+ public static ReadOnlySpan AsSpan(this T[]? array, Index startIndex)
+ {
+#if NET
+ return MemoryExtensions.AsSpan(array, startIndex);
+#else
+ if (array is null)
+ {
+ if (!startIndex.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
+ }
+
+ return default;
+ }
+
+ return MemoryExtensions.AsSpan(array, startIndex.GetOffset(array.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new span over the portion of the target array defined by a value.
+ ///
+ ///
+ /// The array to convert.
+ ///
+ ///
+ /// The range of the array to convert.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ ///
+ /// is covariant, and the array's type is not exactly [].
+ ///
+ public static ReadOnlySpan AsSpan(this T[]? array, Range range)
+ {
+#if NET
+ return MemoryExtensions.AsSpan(array, range);
+#else
+ if (array is null)
+ {
+ if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentNull(nameof(array));
+ }
+
+ return default;
+ }
+
+ var (start, length) = range.GetOffsetAndLength(array.Length);
+ return MemoryExtensions.AsSpan(array, start, length);
+#endif
+ }
+
+ ///
+ /// Creates a new memory region over the portion of the target starting at the specified index
+ /// to the end of the array.
+ ///
+ ///
+ /// The array to convert.
+ ///
+ ///
+ /// The first position of the array.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ ///
+ /// is covariant, and the array's type is not exactly [].
+ ///
+ public static ReadOnlyMemory AsMemory(this T[]? array, Index startIndex)
+ {
+#if NET
+ return MemoryExtensions.AsMemory(array, startIndex);
+#else
+ if (array is null)
+ {
+ if (!startIndex.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
+ }
+
+ return default;
+ }
+
+ return MemoryExtensions.AsMemory(array, startIndex.GetOffset(array.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new memory region over the portion of the target array beginning at
+ /// inclusive start index of the range and ending at the exclusive end index of the range.
+ ///
+ ///
+ /// The array to convert.
+ ///
+ ///
+ /// The range of the array to convert.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is .
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ ///
+ /// is covariant, and the array's type is not exactly [].
+ ///
+ public static ReadOnlyMemory AsMemory(this T[]? array, Range range)
+ {
+#if NET
+ return MemoryExtensions.AsMemory(array, range);
+#else
+ if (array is null)
+ {
+ if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentNull(nameof(array));
+ }
+
+ return default;
+ }
+
+ var (start, length) = range.GetOffsetAndLength(array.Length);
+ return MemoryExtensions.AsMemory(array, start, length);
+#endif
+ }
+
public static ImmutableDictionary ToImmutableDictionary(
this (TKey key, TValue value)[] array, IEqualityComparer keyComparer)
where TKey : notnull
diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs
index e0d7478cca7..3107a833101 100644
--- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs
+++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs
@@ -29,39 +29,6 @@ public static void SetCapacityIfLarger(this ImmutableArray.Builder builder
}
}
- ///
- /// Returns the current contents as an and sets
- /// the collection to a zero length array.
- ///
- ///
- /// If equals
- /// , the internal array will be extracted
- /// as an without copying the contents. Otherwise, the
- /// contents will be copied into a new array. The collection will then be set to a
- /// zero-length array.
- ///
- /// An immutable array.
- public static ImmutableArray DrainToImmutable(this ImmutableArray.Builder builder)
- {
-#if NET8_0_OR_GREATER
- return builder.DrainToImmutable();
-#else
- if (builder.Count == 0)
- {
- return [];
- }
-
- if (builder.Count == builder.Capacity)
- {
- return builder.MoveToImmutable();
- }
-
- var result = builder.ToImmutable();
- builder.Clear();
- return result;
-#endif
- }
-
public static ImmutableArray SelectAsArray(this ImmutableArray source, Func selector)
{
return source switch
diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs
index f649b756075..9dde4e4b539 100644
--- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs
+++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/StringExtensions.cs
@@ -3,27 +3,555 @@
using System.Diagnostics.CodeAnalysis;
+#if !NET
+using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper;
+#endif
+
namespace System;
internal static class StringExtensions
{
- public static bool IsNullOrEmpty([NotNullWhen(false)] this string? s)
- => string.IsNullOrEmpty(s);
+ ///
+ /// Indicates whether the specified string is or an empty string ("").
+ ///
+ ///
+ /// The string to test.
+ ///
+ ///
+ /// if the parameter is
+ /// or an empty string (""); otherwise, .
+ ///
+ ///
+ /// This extension method is useful on .NET Framework and .NET Standard 2.0 where
+ /// is not annotated for nullability.
+ ///
+ public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value)
+ => string.IsNullOrEmpty(value);
+
+ ///
+ /// Indicates whether a specified string is , empty, or consists only
+ /// of white-space characters.
+ ///
+ ///
+ /// The string to test.
+ ///
+ ///
+ /// if the parameter is
+ /// or , or if consists exclusively of
+ /// white-space characters.
+ ///
+ ///
+ /// This extension method is useful on .NET Framework and .NET Standard 2.0 where
+ /// is not annotated for nullability.
+ ///
+ public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value)
+ => string.IsNullOrWhiteSpace(value);
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position to the end of the string.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ public static ReadOnlySpan AsSpan(this string? text, Index startIndex)
+ {
+#if NET
+ return MemoryExtensions.AsSpan(text, startIndex);
+#else
+ if (text is null)
+ {
+ if (!startIndex.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
+ }
+
+ return default;
+ }
+
+ return text.AsSpan(startIndex.GetOffset(text.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new over a portion of a target string using
+ /// the range start and end indexes.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The range that has start and end indexes to use for slicing the string.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ public static ReadOnlySpan AsSpan(this string? text, Range range)
+ {
+#if NET
+ return MemoryExtensions.AsSpan(text, range);
+#else
+ if (text is null)
+ {
+ if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentNull(nameof(text));
+ }
+
+ return default;
+ }
+
+ var (start, length) = range.GetOffsetAndLength(text.Length);
+ return text.AsSpan(start, length);
+#endif
+ }
+
+ ///
+ /// Creates a new over a string. If the target string
+ /// is a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ public static ReadOnlySpan AsSpanOrDefault(this string? text)
+ => text is not null ? text.AsSpan() : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position to the end of the string. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ public static ReadOnlySpan AsSpanOrDefault(this string? text, int start)
+ => text is not null ? text.AsSpan(start) : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position for a specified number of characters. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// The desired length for the slice.
+ ///
+ ///
+ /// , , or +
+ /// is not in the range of .
+ ///
+ public static ReadOnlySpan AsSpanOrDefault(this string? text, int start, int length)
+ => text is not null ? text.AsSpan(start, length) : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position to the end of the string. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ public static ReadOnlySpan AsSpanOrDefault(this string? text, Index startIndex)
+ {
+ if (text is null)
+ {
+ return default;
+ }
+
+#if NET
+ return MemoryExtensions.AsSpan(text, startIndex);
+#else
+ return text.AsSpan(startIndex.GetOffset(text.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new over a portion of the target string using the range
+ /// start and end indexes. If the target string is a
+ /// () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The range that has start and end indexes to use for slicing the string.
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ public static ReadOnlySpan AsSpanOrDefault(this string? text, Range range)
+ {
+ if (text is null)
+ {
+ return default;
+ }
+
+#if NET
+ return MemoryExtensions.AsSpan(text, range);
+#else
+ var (start, length) = range.GetOffsetAndLength(text.Length);
+ return text.AsSpan(start, length);
+#endif
+ }
+
+ ///
+ /// Creates a new over a portion of a target string starting at a specified index.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ public static ReadOnlyMemory AsMemory(this string? text, Index startIndex)
+ {
+#if NET
+ return MemoryExtensions.AsMemory(text, startIndex);
+#else
+ if (text is null)
+ {
+ if (!startIndex.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
+ }
+
+ return default;
+ }
+
+ return text.AsMemory(startIndex.GetOffset(text.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new over a portion of a target string using
+ /// the range start and end indexes.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The range that has start and end indexes to use for slicing the string.
+ ///
+ ///
+ /// This uses Razor's type, which is type-forwarded on .NET.
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ public static ReadOnlyMemory AsMemory(this string? text, Range range)
+ {
+#if NET
+ return MemoryExtensions.AsMemory(text, range);
+#else
+ if (text is null)
+ {
+ if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
+ {
+ ThrowHelper.ThrowArgumentNull(nameof(text));
+ }
+
+ return default;
+ }
+
+ var (start, length) = range.GetOffsetAndLength(text.Length);
+ return text.AsMemory(start, length);
+#endif
+ }
+
+ ///
+ /// Creates a new over a string. If the target string
+ /// is a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ public static ReadOnlyMemory AsMemoryOrDefault(this string? text)
+ => text is not null ? text.AsMemory() : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position to the end of the string. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// is less than 0 or greater than .Length.
+ ///
+ public static ReadOnlyMemory AsMemoryOrDefault(this string? text, int start)
+ => text is not null ? text.AsMemory(start) : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position for a specified number of characters. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ ///
+ /// The desired length for the slice.
+ ///
+ ///
+ /// , , or +
+ /// is not in the range of .
+ ///
+ public static ReadOnlyMemory AsMemoryOrDefault(this string? text, int start, int length)
+ => text is not null ? text.AsMemory(start, length) : default;
+
+ ///
+ /// Creates a new over a portion of the target string from
+ /// a specified position to the end of the string. If the target string is
+ /// a () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The index at which to begin this slice.
+ ///
+ public static ReadOnlyMemory AsMemoryOrDefault(this string? text, Index startIndex)
+ {
+ if (text is null)
+ {
+ return default;
+ }
+
+#if NET
+ return MemoryExtensions.AsMemory(text, startIndex);
+#else
+ return text.AsMemory(startIndex.GetOffset(text.Length));
+#endif
+ }
+
+ ///
+ /// Creates a new over a portion of the target string using the range
+ /// start and end indexes. If the target string is a
+ /// () is returned.
+ ///
+ ///
+ /// The target string.
+ ///
+ ///
+ /// The range that has start and end indexes to use for slicing the string.
+ ///
+ ///
+ /// 's start or end index is not within the bounds of the string.
+ ///
+ ///
+ /// 's start index is greater than its end index.
+ ///
+ public static ReadOnlyMemory AsMemoryOrDefault(this string? text, Range range)
+ {
+ if (text is null)
+ {
+ return default;
+ }
+
+#if NET
+ return MemoryExtensions.AsMemory(text, range);
+#else
+ var (start, length) = range.GetOffsetAndLength(text.Length);
+ return text.AsMemory(start, length);
+#endif
+ }
- public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? s)
- => string.IsNullOrWhiteSpace(s);
+ ///
+ /// Returns a value indicating whether a specified character occurs within a string instance.
+ ///
+ ///
+ /// The string instance.
+ ///
+ ///
+ /// The character to seek.
+ ///
+ ///
+ /// if the value parameter occurs within the string; otherwise, .
+ ///
+ ///
+ /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0.
+ ///
+ public static bool Contains(this string text, char value)
+ {
+#if NET
+ return text.Contains(value);
+#else
+ return text.IndexOf(value) >= 0;
+#endif
+ }
- public static ReadOnlySpan AsSpanOrDefault(this string? s)
- => s is not null ? s.AsSpan() : default;
+ ///
+ /// Returns a value indicating whether a specified character occurs within a string instance,
+ /// using the specified comparison rules.
+ ///
+ ///
+ /// The string instance.
+ ///
+ ///
+ /// The character to seek.
+ ///
+ ///
+ /// One of the enumeration values that specifies the rules to use in the comparison.
+ ///
+ ///
+ /// if the value parameter occurs within the string; otherwise, .
+ ///
+ ///
+ /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0.
+ ///
+ public static bool Contains(this string text, char value, StringComparison comparisonType)
+ {
+#if NET
+ return text.Contains(value, comparisonType);
+#else
+ return text.IndexOf(value, comparisonType) != 0;
+#endif
+ }
- public static ReadOnlyMemory AsMemoryOrDefault(this string? s)
- => s is not null ? s.AsMemory() : default;
+ ///
+ /// Reports the zero-based index of the first occurrence of the specified Unicode character in a string instance.
+ /// A parameter specifies the type of search to use for the specified character.
+ ///
+ ///
+ /// The string instance.
+ ///
+ ///
+ /// The character to compare to the character at the start of this string.
+ ///
+ ///
+ /// An enumeration value that specifies the rules for the search.
+ ///
+ ///
+ /// The zero-based index of if that character is found, or -1 if it is not.
+ ///
+ ///
+ ///
+ /// Index numbering starts from zero.
+ ///
+ ///
+ /// The parameter is a enumeration member
+ /// that specifies whether the search for the argument uses the current or invariant culture,
+ /// is case-sensitive or case-insensitive, or uses word or ordinal comparison rules.
+ ///
+ ///
+ /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0.
+ ///
+ ///
+ public static int IndexOf(this string text, char value, StringComparison comparisonType)
+ {
+#if NET
+ return text.IndexOf(value, comparisonType);
+#else
+ // [ch] produces a ReadOnlySpan using a ref to ch.
+ return text.AsSpan().IndexOf([value], comparisonType);
+#endif
+ }
- // This method doesn't exist on .NET Framework, but it does on .NET Core.
- public static bool Contains(this string s, char ch)
- => s.IndexOf(ch) >= 0;
+ ///
+ /// Determines whether a string instance starts with the specified character.
+ ///
+ ///
+ /// The string instance.
+ ///
+ ///
+ /// The character to compare to the character at the start of this string.
+ ///
+ ///
+ /// if matches the start of the string;
+ /// otherwise, .
+ ///
+ ///
+ ///
+ /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison.
+ ///
+ ///
+ /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0.
+ ///
+ ///
+ public static bool StartsWith(this string text, char value)
+ {
+#if NET
+ return text.StartsWith(value);
+#else
+ return text.Length > 0 && text[0] == value;
+#endif
+ }
- // This method doesn't exist on .NET Framework, but it does on .NET Core.
- public static bool EndsWith(this string s, char ch)
- => s.Length > 0 && s[^1] == ch;
+ ///
+ /// Determines whether the end of a string instance matches the specified character.
+ ///
+ ///
+ /// The string instance.
+ ///
+ ///
+ /// The character to compare to the character at the end of this string.
+ ///
+ ///
+ /// if matches the end of this string;
+ /// otherwise, .
+ ///
+ ///
+ ///
+ /// This method performs an ordinal (case-sensitive and culture-insensitive) comparison.
+ ///
+ ///
+ /// This method exists on .NET Core, but doesn't on .NET Framework or .NET Standard 2.0.
+ ///
+ ///
+ public static bool EndsWith(this string text, char value)
+ {
+#if NET
+ return text.EndsWith(value);
+#else
+ return text.Length > 0 && text[^1] == value;
+#endif
+ }
}
diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs
index 1a78ee3a3d2..168871c0218 100644
--- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs
+++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Utilities/ThrowHelper.cs
@@ -8,6 +8,34 @@ namespace Microsoft.AspNetCore.Razor.Utilities;
internal static class ThrowHelper
{
+ ///
+ /// This is present to help the JIT inline methods that need to throw an .
+ ///
+ [DoesNotReturn]
+ public static void ThrowArgumentNull(string paramName)
+ => throw new ArgumentNullException(paramName);
+
+ ///
+ /// This is present to help the JIT inline methods that need to throw an .
+ ///
+ [DoesNotReturn]
+ public static T ThrowArgumentNull(string paramName)
+ => throw new ArgumentNullException(paramName);
+
+ ///
+ /// This is present to help the JIT inline methods that need to throw an .
+ ///
+ [DoesNotReturn]
+ public static void ThrowArgumentOutOfRange(string paramName)
+ => throw new ArgumentOutOfRangeException(paramName);
+
+ ///
+ /// This is present to help the JIT inline methods that need to throw an .
+ ///
+ [DoesNotReturn]
+ public static T ThrowArgumentOutOfRange(string paramName)
+ => throw new ArgumentOutOfRangeException(paramName);
+
///
/// This is present to help the JIT inline methods that need to throw an .
///