2021-02-19 16:50:05 +01:00
// Copyright (c) Umbraco.
2021-02-18 11:06:02 +01:00
// See LICENSE for more details.
2019-05-20 17:57:28 +02:00
using System.ComponentModel ;
using System.Diagnostics.CodeAnalysis ;
using System.Globalization ;
using System.Security.Cryptography ;
using System.Text ;
using System.Text.RegularExpressions ;
2021-02-18 11:06:02 +01:00
using Umbraco.Cms.Core ;
using Umbraco.Cms.Core.Strings ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
namespace Umbraco.Extensions ;
/// <summary>
/// String extension methods
/// </summary>
public static class StringExtensions
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
internal static readonly Lazy < Regex > Whitespace = new ( ( ) = > new Regex ( @"\s+" , RegexOptions . Compiled ) ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
private const char DefaultEscapedStringEscapeChar = '\\' ;
private static readonly char [ ] ToCSharpHexDigitLower = "0123456789abcdef" . ToCharArray ( ) ;
private static readonly char [ ] ToCSharpEscapeChars ;
internal static readonly string [ ] JsonEmpties = { "[]" , "{}" } ;
/// <summary>
/// The namespace for URLs (from RFC 4122, Appendix C).
/// See <a href="http://www.ietf.org/rfc/rfc4122.txt">RFC 4122</a>
/// </summary>
internal static readonly Guid UrlNamespace = new ( "6ba7b811-9dad-11d1-80b4-00c04fd430c8" ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
private static readonly char [ ] CleanForXssChars = "*?(){}[];:%<>/\\|&'\"" . ToCharArray ( ) ;
// From: http://stackoverflow.com/a/961504/5018
// filters control characters but allows only properly-formed surrogate sequences
private static readonly Lazy < Regex > InvalidXmlChars = new ( ( ) = >
new Regex (
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]" ,
RegexOptions . Compiled ) ) ;
static StringExtensions ( )
{
var escapes = new [ ] { "\aa" , "\bb" , "\ff" , "\nn" , "\rr" , "\tt" , "\vv" , "\"\"" , "\\\\" , "??" , "\00" } ;
ToCSharpEscapeChars = new char [ escapes . Max ( e = > e [ 0 ] ) + 1 ] ;
foreach ( var escape in escapes )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
ToCSharpEscapeChars [ escape [ 0 ] ] = escape [ 1 ] ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Convert a path to node ids in the order from right to left (deepest to shallowest)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static int [ ] GetIdsFromPathReversed ( this string path )
{
var nodeIds = path . Split ( Constants . CharArrays . Comma , StringSplitOptions . RemoveEmptyEntries )
. Select ( x = >
int . TryParse ( x , NumberStyles . Integer , CultureInfo . InvariantCulture , out var output )
? Attempt < int > . Succeed ( output )
: Attempt < int > . Fail ( ) )
. Where ( x = > x . Success )
. Select ( x = > x . Result )
. Reverse ( )
. ToArray ( ) ;
return nodeIds ;
}
/// <summary>
/// Removes new lines and tabs
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static string StripWhitespace ( this string txt ) = > Regex . Replace ( txt , @"\s" , string . Empty ) ;
public static string StripFileExtension ( this string fileName )
{
// filenames cannot contain line breaks
if ( fileName . Contains ( Environment . NewLine ) | | fileName . Contains ( "\r" ) | | fileName . Contains ( "\n" ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return fileName ;
2019-05-20 17:57:28 +02:00
}
2022-11-28 22:12:30 +01:00
var spanFileName = fileName . AsSpan ( ) ;
var lastIndex = spanFileName . LastIndexOf ( '.' ) ;
2022-06-07 15:28:38 +02:00
if ( lastIndex > 0 )
2019-05-20 17:57:28 +02:00
{
2022-11-28 22:12:30 +01:00
var ext = spanFileName [ lastIndex . . ] ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// file extensions cannot contain whitespace
2022-11-28 22:12:30 +01:00
if ( ext . Contains ( ' ' ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return fileName ;
2019-05-20 17:57:28 +02:00
}
2022-11-28 22:12:30 +01:00
return new string ( spanFileName [ . . lastIndex ] ) ;
2022-06-07 15:28:38 +02:00
}
2018-06-29 19:52:40 +02:00
2022-06-07 15:28:38 +02:00
return fileName ;
}
2018-06-29 19:52:40 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Determines the extension of the path or URL
/// </summary>
/// <param name="file"></param>
/// <returns>Extension of the file</returns>
public static string GetFileExtension ( this string file )
{
// Find any characters between the last . and the start of a query string or the end of the string
const string pattern = @"(?<extension>\.[^\.\?]+)(\?.*|$)" ;
Match match = Regex . Match ( file , pattern ) ;
return match . Success
? match . Groups [ "extension" ] . Value
: string . Empty ;
}
2018-06-29 19:52:40 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// This tries to detect a json string, this is not a fail safe way but it is quicker than doing
/// a try/catch when deserializing when it is not json.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static bool DetectIsJson ( this string input )
{
if ( input . IsNullOrWhiteSpace ( ) )
2020-03-24 13:35:11 -04:00
{
2022-06-07 15:28:38 +02:00
return false ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
input = input . Trim ( ) ;
2022-10-05 12:14:43 +02:00
return ( input [ 0 ] is '[' & & input [ ^ 1 ] is ']' ) | | ( input [ 0 ] is '{' & & input [ ^ 1 ] is '}' ) ;
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static bool DetectIsEmptyJson ( this string input ) = >
JsonEmpties . Contains ( Whitespace . Value . Replace ( input , string . Empty ) ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string ReplaceNonAlphanumericChars ( this string input , string replacement )
{
// any character that is not alphanumeric, convert to a hyphen
var mName = input ;
foreach ( var c in mName . ToCharArray ( ) . Where ( c = > ! char . IsLetterOrDigit ( c ) ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
mName = mName . Replace ( c . ToString ( CultureInfo . InvariantCulture ) , replacement ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return mName ;
}
public static string ReplaceNonAlphanumericChars ( this string input , char replacement )
{
var inputArray = input . ToCharArray ( ) ;
var outputArray = new char [ input . Length ] ;
for ( var i = 0 ; i < inputArray . Length ; i + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
outputArray [ i ] = char . IsLetterOrDigit ( inputArray [ i ] ) ? inputArray [ i ] : replacement ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return new string ( outputArray ) ;
}
/// <summary>
/// Cleans string to aid in preventing xss attacks.
/// </summary>
/// <param name="input"></param>
/// <param name="ignoreFromClean"></param>
/// <returns></returns>
public static string CleanForXss ( this string input , params char [ ] ignoreFromClean )
{
// remove any HTML
input = input . StripHtml ( ) ;
// strip out any potential chars involved with XSS
return input . ExceptChars ( new HashSet < char > ( CleanForXssChars . Except ( ignoreFromClean ) ) ) ;
}
public static string ExceptChars ( this string str , HashSet < char > toExclude )
{
var sb = new StringBuilder ( str . Length ) ;
foreach ( var c in str . Where ( c = > toExclude . Contains ( c ) = = false ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
sb . Append ( c ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return sb . ToString ( ) ;
}
/// <summary>
/// This will append the query string to the URL
/// </summary>
/// <param name="url"></param>
/// <param name="queryStrings"></param>
/// <returns></returns>
/// <remarks>
/// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are
2023-09-06 20:08:17 +02:00
/// delimited properly with '&'
2022-06-07 15:28:38 +02:00
/// </remarks>
public static string AppendQueryStringToUrl ( this string url , params string [ ] queryStrings )
{
// remove any prefixed '&' or '?'
for ( var i = 0 ; i < queryStrings . Length ; i + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
queryStrings [ i ] = queryStrings [ i ] . TrimStart ( Constants . CharArrays . QuestionMarkAmpersand )
. TrimEnd ( Constants . CharArrays . Ampersand ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
var nonEmpty = queryStrings . Where ( x = > ! x . IsNullOrWhiteSpace ( ) ) . ToArray ( ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( url . Contains ( "?" ) )
{
return url + string . Join ( "&" , nonEmpty ) . EnsureStartsWith ( '&' ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return url + string . Join ( "&" , nonEmpty ) . EnsureStartsWith ( '?' ) ;
}
/// <summary>
/// Returns a stream from a string
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
internal static Stream GenerateStreamFromString ( this string s )
{
var stream = new MemoryStream ( ) ;
var writer = new StreamWriter ( stream ) ;
writer . Write ( s ) ;
writer . Flush ( ) ;
stream . Position = 0 ;
return stream ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// this is from SqlMetal and just makes it a bit of fun to allow pluralization
public static string MakePluralName ( this string name )
{
if ( name . EndsWith ( "x" , StringComparison . OrdinalIgnoreCase ) | |
name . EndsWith ( "ch" , StringComparison . OrdinalIgnoreCase ) | |
name . EndsWith ( "s" , StringComparison . OrdinalIgnoreCase ) | |
name . EndsWith ( "sh" , StringComparison . OrdinalIgnoreCase ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
name + = "es" ;
2019-05-20 17:57:28 +02:00
return name ;
}
2022-06-07 15:28:38 +02:00
if ( name . EndsWith ( "y" , StringComparison . OrdinalIgnoreCase ) & & name . Length > 1 & &
! IsVowel ( name [ ^ 2 ] ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
name = name . Remove ( name . Length - 1 , 1 ) ;
name + = "ies" ;
return name ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( ! name . EndsWith ( "s" , StringComparison . OrdinalIgnoreCase ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
name + = "s" ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return name ;
}
public static bool IsVowel ( this char c )
{
switch ( c )
{
case 'O' :
case 'U' :
case 'Y' :
case 'A' :
case 'E' :
case 'I' :
case 'o' :
case 'u' :
case 'y' :
case 'a' :
case 'e' :
case 'i' :
return true ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return false ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Trims the specified value from a string; accepts a string input whereas the in-built implementation only accepts
/// char or char[].
/// </summary>
/// <param name="value">The value.</param>
/// <param name="forRemoving">For removing.</param>
/// <returns></returns>
2024-07-01 09:17:48 +02:00
public static string TrimExact ( this string value , string forRemoving )
2022-06-07 15:28:38 +02:00
{
if ( string . IsNullOrEmpty ( value ) )
{
2019-05-20 17:57:28 +02:00
return value ;
}
2024-07-01 09:17:48 +02:00
return value . TrimEndExact ( forRemoving ) . TrimStartExact ( forRemoving ) ;
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string EncodeJsString ( this string s )
{
var sb = new StringBuilder ( ) ;
foreach ( var c in s )
{
switch ( c )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
case '\"' :
sb . Append ( "\\\"" ) ;
break ;
case '\\' :
sb . Append ( "\\\\" ) ;
break ;
case '\b' :
sb . Append ( "\\b" ) ;
break ;
case '\f' :
sb . Append ( "\\f" ) ;
break ;
case '\n' :
sb . Append ( "\\n" ) ;
break ;
case '\r' :
sb . Append ( "\\r" ) ;
break ;
case '\t' :
sb . Append ( "\\t" ) ;
break ;
default :
int i = c ;
if ( i < 32 | | i > 127 )
{
sb . AppendFormat ( "\\u{0:X04}" , i ) ;
}
else
{
sb . Append ( c ) ;
}
break ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
return sb . ToString ( ) ;
}
2024-07-01 09:17:48 +02:00
public static string TrimEndExact ( this string value , string forRemoving )
2022-06-07 15:28:38 +02:00
{
if ( string . IsNullOrEmpty ( value ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return value ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrEmpty ( forRemoving ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return value ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
while ( value . EndsWith ( forRemoving , StringComparison . InvariantCultureIgnoreCase ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
value = value . Remove ( value . LastIndexOf ( forRemoving , StringComparison . InvariantCultureIgnoreCase ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return value ;
}
2024-07-01 09:17:48 +02:00
public static string TrimStartExact ( this string value , string forRemoving )
2022-06-07 15:28:38 +02:00
{
if ( string . IsNullOrEmpty ( value ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return value ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrEmpty ( forRemoving ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return value ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
while ( value . StartsWith ( forRemoving , StringComparison . InvariantCultureIgnoreCase ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
value = value . Substring ( forRemoving . Length ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return value ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string EnsureStartsWith ( this string input , string toStartWith )
{
if ( input . StartsWith ( toStartWith ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return input ;
2019-05-20 17:57:28 +02:00
}
2024-07-01 09:17:48 +02:00
return toStartWith + input . TrimStartExact ( toStartWith ) ;
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string EnsureStartsWith ( this string input , char value ) = >
input . StartsWith ( value . ToString ( CultureInfo . InvariantCulture ) ) ? input : value + input ;
public static string EnsureEndsWith ( this string input , char value ) = >
input . EndsWith ( value . ToString ( CultureInfo . InvariantCulture ) ) ? input : input + value ;
public static string EnsureEndsWith ( this string input , string toEndWith ) = >
input . EndsWith ( toEndWith . ToString ( CultureInfo . InvariantCulture ) ) ? input : input + toEndWith ;
public static bool IsLowerCase ( this char ch ) = > ch . ToString ( CultureInfo . InvariantCulture ) = =
ch . ToString ( CultureInfo . InvariantCulture ) . ToLowerInvariant ( ) ;
public static bool IsUpperCase ( this char ch ) = > ch . ToString ( CultureInfo . InvariantCulture ) = =
ch . ToString ( CultureInfo . InvariantCulture ) . ToUpperInvariant ( ) ;
/// <summary>
/// Indicates whether a specified string is null, empty, or
/// consists only of white-space characters.
/// </summary>
/// <param name="value">The value to check.</param>
/// <returns>
/// Returns <see langword="true" /> if the value is null,
/// empty, or consists only of white-space characters, otherwise
/// returns <see langword="false" />.
/// </returns>
2022-10-05 12:14:43 +02:00
public static bool IsNullOrWhiteSpace ( [ NotNullWhen ( false ) ] this string? value ) = > string . IsNullOrWhiteSpace ( value ) ;
2023-11-14 11:16:12 +01:00
2022-09-16 01:14:23 +02:00
[return: NotNullIfNotNull("defaultValue")]
2022-06-07 15:28:38 +02:00
public static string? IfNullOrWhiteSpace ( this string? str , string? defaultValue ) = >
str . IsNullOrWhiteSpace ( ) ? defaultValue : str ;
/// <summary>The to delimited list.</summary>
/// <param name="list">The list.</param>
/// <param name="delimiter">The delimiter.</param>
/// <returns>the list</returns>
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "By design")]
public static IList < string > ToDelimitedList ( this string list , string delimiter = "," )
{
var delimiters = new [ ] { delimiter } ;
return ! list . IsNullOrWhiteSpace ( )
? list . Split ( delimiters , StringSplitOptions . RemoveEmptyEntries )
. Select ( i = > i . Trim ( ) )
. ToList ( )
: new List < string > ( ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>enum try parse.</summary>
/// <param name="strType">The str type.</param>
/// <param name="ignoreCase">The ignore case.</param>
/// <param name="result">The result.</param>
/// <typeparam name="T">The type</typeparam>
/// <returns>The enum try parse.</returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
public static bool EnumTryParse < T > ( this string strType , bool ignoreCase , out T ? result )
{
try
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
result = ( T ) Enum . Parse ( typeof ( T ) , strType , ignoreCase ) ;
return true ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
catch
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
result = default ;
return false ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Parse string to Enum
/// </summary>
/// <typeparam name="T">The enum type</typeparam>
/// <param name="strType">The string to parse</param>
/// <param name="ignoreCase">The ignore case</param>
/// <returns>The parsed enum</returns>
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "By Design")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By Design")]
public static T EnumParse < T > ( this string strType , bool ignoreCase ) = > ( T ) Enum . Parse ( typeof ( T ) , strType , ignoreCase ) ;
/// <summary>
/// Strips all HTML from a string.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>Returns the string without any HTML tags.</returns>
public static string StripHtml ( this string text )
{
const string pattern = @"<(.|\n)*?>" ;
return Regex . Replace ( text , pattern , string . Empty , RegexOptions . Compiled ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Encodes as GUID.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static Guid EncodeAsGuid ( this string input )
{
if ( string . IsNullOrWhiteSpace ( input ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( "input" ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
var convertToHex = input . ConvertToHex ( ) ;
var hexLength = convertToHex . Length < 32 ? convertToHex . Length : 32 ;
var hex = convertToHex . Substring ( 0 , hexLength ) . PadLeft ( 32 , '0' ) ;
Guid output = Guid . Empty ;
return Guid . TryParse ( hex , out output ) ? output : Guid . Empty ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Converts to hex.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static string ConvertToHex ( this string input )
{
if ( string . IsNullOrEmpty ( input ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return string . Empty ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
var sb = new StringBuilder ( input . Length ) ;
foreach ( var c in input )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
sb . AppendFormat ( "{0:x2}" , Convert . ToUInt32 ( c ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return sb . ToString ( ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string DecodeFromHex ( this string hexValue )
{
var strValue = string . Empty ;
while ( hexValue . Length > 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
strValue + = Convert . ToChar ( Convert . ToUInt32 ( hexValue . Substring ( 0 , 2 ) , 16 ) ) . ToString ( ) ;
hexValue = hexValue . Substring ( 2 , hexValue . Length - 2 ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return strValue ;
}
/// <summary>
/// Encodes a string to a safe URL base64 string
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string ToUrlBase64 ( this string input )
{
if ( input = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( input ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrEmpty ( input ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return string . Empty ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
var bytes = Encoding . UTF8 . GetBytes ( input ) ;
return UrlTokenEncode ( bytes ) ;
}
/// <summary>
/// Decodes a URL safe base64 string back
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string? FromUrlBase64 ( this string input )
{
if ( input = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( input ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// if (input.IsInvalidBase64()) return null;
try
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "="));
var decodedBytes = UrlTokenDecode ( input ) ;
return decodedBytes ! = null ? Encoding . UTF8 . GetString ( decodedBytes ) : null ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
catch ( FormatException )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return null ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// formats the string with invariant culture
/// </summary>
/// <param name="format">The format.</param>
/// <param name="args">The args.</param>
/// <returns></returns>
public static string InvariantFormat ( this string? format , params object? [ ] args ) = >
string . Format ( CultureInfo . InvariantCulture , format ? ? string . Empty , args ) ;
/// <summary>
/// Converts an integer to an invariant formatted string
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string ToInvariantString ( this int str ) = > str . ToString ( CultureInfo . InvariantCulture ) ;
public static string ToInvariantString ( this long str ) = > str . ToString ( CultureInfo . InvariantCulture ) ;
/// <summary>
/// Compares 2 strings with invariant culture and case ignored
/// </summary>
/// <param name="compare">The compare.</param>
/// <param name="compareTo">The compare to.</param>
/// <returns></returns>
public static bool InvariantEquals ( this string? compare , string? compareTo ) = >
string . Equals ( compare , compareTo , StringComparison . InvariantCultureIgnoreCase ) ;
public static bool InvariantStartsWith ( this string compare , string compareTo ) = >
compare . StartsWith ( compareTo , StringComparison . InvariantCultureIgnoreCase ) ;
public static bool InvariantEndsWith ( this string compare , string compareTo ) = >
compare . EndsWith ( compareTo , StringComparison . InvariantCultureIgnoreCase ) ;
public static bool InvariantContains ( this string compare , string compareTo ) = >
compare . IndexOf ( compareTo , StringComparison . OrdinalIgnoreCase ) > = 0 ;
public static bool InvariantContains ( this IEnumerable < string > compare , string compareTo ) = >
compare . Contains ( compareTo , StringComparer . InvariantCultureIgnoreCase ) ;
public static int InvariantIndexOf ( this string s , string value ) = >
s . IndexOf ( value , StringComparison . OrdinalIgnoreCase ) ;
public static int InvariantLastIndexOf ( this string s , string value ) = >
s . LastIndexOf ( value , StringComparison . OrdinalIgnoreCase ) ;
/// <summary>
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="val"></param>
/// <returns></returns>
public static T ? ParseInto < T > ( this string val ) = > ( T ? ) val . ParseInto ( typeof ( T ) ) ;
/// <summary>
/// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method
/// </summary>
/// <param name="val"></param>
/// <param name="type"></param>
/// <returns></returns>
public static object? ParseInto ( this string val , Type type )
{
if ( string . IsNullOrEmpty ( val ) = = false )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
TypeConverter tc = TypeDescriptor . GetConverter ( type ) ;
return tc . ConvertFrom ( val ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return val ;
}
/// <summary>
/// Generates a hash of a string based on the FIPS compliance setting.
/// </summary>
/// <param name="str">Refers to itself</param>
/// <returns>The hashed string</returns>
public static string GenerateHash ( this string str ) = > str . ToSHA1 ( ) ;
/// <summary>
/// Generate a hash of a string based on the specified hash algorithm.
/// </summary>
/// <typeparam name="T">The hash algorithm implementation to use.</typeparam>
/// <param name="str">The <see cref="string" /> to hash.</param>
/// <returns>
/// The hashed string.
/// </returns>
public static string GenerateHash < T > ( this string str )
where T : HashAlgorithm = > str . GenerateHash ( typeof ( T ) . FullName ) ;
/// <summary>
/// Converts the string to SHA1
/// </summary>
/// <param name="stringToConvert">refers to itself</param>
/// <returns>The SHA1 hashed string</returns>
public static string ToSHA1 ( this string stringToConvert ) = > stringToConvert . GenerateHash ( "SHA1" ) ;
/// <summary>
/// Decodes a string that was encoded with UrlTokenEncode
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static byte [ ] UrlTokenDecode ( this string input )
{
if ( input = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( input ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( input . Length = = 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return Array . Empty < byte > ( ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// calc array size - must be groups of 4
var arrayLength = input . Length ;
var remain = arrayLength % 4 ;
if ( remain ! = 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
arrayLength + = 4 - remain ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
var inArray = new char [ arrayLength ] ;
for ( var i = 0 ; i < input . Length ; i + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
var ch = input [ i ] ;
switch ( ch )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
case '-' : // restore '-' as '+'
inArray [ i ] = '+' ;
break ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case '_' : // restore '_' as '/'
inArray [ i ] = '/' ;
break ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
default : // keep char unchanged
inArray [ i ] = ch ;
break ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
// pad with '='
for ( var j = input . Length ; j < inArray . Length ; j + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
inArray [ j ] = '=' ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
return Convert . FromBase64CharArray ( inArray , 0 , inArray . Length ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Generate a hash of a string based on the hashType passed in
/// </summary>
/// <param name="str">Refers to itself</param>
/// <param name="hashType">
/// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a
/// list of possible values.
/// </param>
/// <returns>The hashed string</returns>
private static string GenerateHash ( this string str , string? hashType )
{
HashAlgorithm ? hasher = null ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// create an instance of the correct hashing provider based on the type passed in
if ( hashType is not null )
{
hasher = HashAlgorithm . Create ( hashType ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( hasher = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new InvalidOperationException ( "No hashing type found by name " + hashType ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
using ( hasher )
{
// convert our string into byte array
var byteArray = Encoding . UTF8 . GetBytes ( str ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// get the hashed values created by our selected provider
var hashedByteArray = hasher . ComputeHash ( byteArray ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// create a StringBuilder object
var stringBuilder = new StringBuilder ( ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// loop to each byte
foreach ( var b in hashedByteArray )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
// append it to our StringBuilder
stringBuilder . Append ( b . ToString ( "x2" ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// return the hashed value
return stringBuilder . ToString ( ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Encodes a string so that it is 'safe' for URLs, files, etc..
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string UrlTokenEncode ( this byte [ ] input )
{
if ( input = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( input ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( input . Length = = 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return string . Empty ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// base-64 digits are A-Z, a-z, 0-9, + and /
// the = char is used for trailing padding
var str = Convert . ToBase64String ( input ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
var pos = str . IndexOf ( '=' ) ;
if ( pos < 0 )
{
pos = str . Length ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// replace chars that would cause problems in URLs
var chArray = new char [ pos ] ;
for ( var i = 0 ; i < pos ; i + + )
{
var ch = str [ i ] ;
switch ( ch )
{
case '+' : // replace '+' with '-'
chArray [ i ] = '-' ;
break ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
case '/' : // replace '/' with '_'
chArray [ i ] = '_' ;
break ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
default : // keep char unchanged
chArray [ i ] = ch ;
break ;
}
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return new string ( chArray ) ;
}
/// <summary>
/// Ensures that the folder path ends with a DirectorySeparatorChar
/// </summary>
/// <param name="currentFolder"></param>
/// <returns></returns>
public static string NormaliseDirectoryPath ( this string currentFolder )
{
currentFolder = currentFolder
. IfNull ( x = > string . Empty )
. TrimEnd ( Path . DirectorySeparatorChar ) + Path . DirectorySeparatorChar ;
return currentFolder ;
}
/// <summary>
/// Truncates the specified text string.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="maxLength">Length of the max.</param>
/// <param name="suffix">The suffix.</param>
/// <returns></returns>
public static string Truncate ( this string text , int maxLength , string suffix = "..." )
{
// replaces the truncated string to a ...
var truncatedString = text ;
if ( maxLength < = 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return truncatedString ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
var strLength = maxLength - suffix . Length ;
if ( strLength < = 0 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return truncatedString ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( text = = null | | text . Length < = maxLength )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return truncatedString ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
truncatedString = text . Substring ( 0 , strLength ) ;
truncatedString = truncatedString . TrimEnd ( ) ;
truncatedString + = suffix ;
return truncatedString ;
}
/// <summary>
/// Strips carrage returns and line feeds from the specified text.
/// </summary>
/// <param name="input">The input.</param>
/// <returns></returns>
public static string StripNewLines ( this string input ) = > input . Replace ( "\r" , string . Empty ) . Replace ( "\n" , string . Empty ) ;
/// <summary>
/// Converts to single line by replacing line breaks with spaces.
/// </summary>
public static string ToSingleLine ( this string text )
{
if ( string . IsNullOrEmpty ( text ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return text ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
text = text . Replace ( "\r\n" , " " ) ; // remove CRLF
text = text . Replace ( "\r" , " " ) ; // remove CR
text = text . Replace ( "\n" , " " ) ; // remove LF
return text ;
}
public static string OrIfNullOrWhiteSpace ( this string input , string alternative ) = >
! string . IsNullOrWhiteSpace ( input )
? input
: alternative ;
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpper ( this string input ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToUpper ( ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLower ( this string input ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToLower ( ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
/// specified culture.
/// </summary>
/// <param name="input">The string.</param>
/// <param name="culture">The culture.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpper ( this string input , CultureInfo culture ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToUpper ( culture ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
/// specified culture.
/// </summary>
/// <param name="input">The string.</param>
/// <param name="culture">The culture.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLower ( this string input , CultureInfo culture ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToLower ( culture ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a copy of the string with the first character converted to uppercase using the casing rules of the
/// invariant culture.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstUpperInvariant ( this string input ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToUpperInvariant ( ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a copy of the string with the first character converted to lowercase using the casing rules of the
/// invariant culture.
/// </summary>
/// <param name="input">The string.</param>
/// <returns>The converted string.</returns>
public static string ToFirstLowerInvariant ( this string input ) = >
string . IsNullOrWhiteSpace ( input )
? input
: input . Substring ( 0 , 1 ) . ToLowerInvariant ( ) + input . Substring ( 1 ) ;
/// <summary>
/// Returns a new string in which all occurrences of specified strings are replaced by other specified strings.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="replacements">The replacements definition.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceMany ( this string text , IDictionary < string , string > replacements )
{
if ( text = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( text ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( replacements = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( replacements ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
foreach ( KeyValuePair < string , string > item in replacements )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
text = text . Replace ( item . Key , item . Value ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return text ;
}
/// <summary>
/// Returns a new string in which all occurrences of specified characters are replaced by a specified character.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="chars">The characters to replace.</param>
/// <param name="replacement">The replacement character.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceMany ( this string text , char [ ] chars , char replacement )
{
if ( text = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( text ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( chars = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( chars ) ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
for ( var i = 0 ; i < chars . Length ; i + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
text = text . Replace ( chars [ i ] , replacement ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
return text ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Returns a new string in which only the first occurrence of a specified string is replaced by a specified
/// replacement string.
/// </summary>
/// <param name="text">The string to filter.</param>
/// <param name="search">The string to replace.</param>
/// <param name="replace">The replacement string.</param>
/// <returns>The filtered string.</returns>
public static string ReplaceFirst ( this string text , string search , string replace )
{
if ( text = = null )
{
throw new ArgumentNullException ( nameof ( text ) ) ;
}
2019-05-20 17:57:28 +02:00
2023-03-23 21:11:59 +01:00
ReadOnlySpan < char > spanText = text . AsSpan ( ) ;
var pos = spanText . IndexOf ( search , StringComparison . InvariantCulture ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( pos < 0 )
{
2019-05-20 17:57:28 +02:00
return text ;
}
2023-03-23 21:11:59 +01:00
return string . Concat ( spanText [ . . pos ] , replace . AsSpan ( ) , spanText [ ( pos + search . Length ) . . ] ) ;
2022-06-07 15:28:38 +02:00
}
2020-03-31 16:28:13 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// An extension method that returns a new string in which all occurrences of a
/// specified string in the current instance are replaced with another specified string.
/// StringComparison specifies the type of search to use for the specified string.
/// </summary>
/// <param name="source">Current instance of the string</param>
/// <param name="oldString">Specified string to replace</param>
/// <param name="newString">Specified string to inject</param>
/// <param name="stringComparison">String Comparison object to specify search type</param>
/// <returns>Updated string</returns>
public static string Replace ( this string source , string oldString , string newString , StringComparison stringComparison )
{
// This initialization ensures the first check starts at index zero of the source. On successive checks for
// a match, the source is skipped to immediately after the last replaced occurrence for efficiency
// and to avoid infinite loops when oldString and newString compare equal.
var index = - 1 * newString . Length ;
2020-03-17 10:21:43 +01:00
2022-06-07 15:28:38 +02:00
// Determine if there are any matches left in source, starting from just after the result of replacing the last match.
while ( ( index = source . IndexOf ( oldString , index + newString . Length , stringComparison ) ) > = 0 )
{
// Remove the old text.
source = source . Remove ( index , oldString . Length ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// Add the replacement text.
source = source . Insert ( index , newString ) ;
2020-03-17 10:21:43 +01:00
}
2019-10-30 15:28:15 +01:00
2022-06-07 15:28:38 +02:00
return source ;
}
2019-10-30 15:28:15 +01:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Converts a literal string into a C# expression.
/// </summary>
/// <param name="s">Current instance of the string.</param>
/// <returns>The string in a C# format.</returns>
public static string ToCSharpString ( this string s )
{
if ( s = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
return "<null>" ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// http://stackoverflow.com/questions/323640/can-i-convert-a-c-sharp-string-value-to-an-escaped-string-literal
var sb = new StringBuilder ( s . Length + 2 ) ;
for ( var rp = 0 ; rp < s . Length ; rp + + )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
var c = s [ rp ] ;
if ( c < ToCSharpEscapeChars . Length & & ToCSharpEscapeChars [ c ] ! = '\0' )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
sb . Append ( '\\' ) . Append ( ToCSharpEscapeChars [ c ] ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
else if ( c < = '~' & & c > = ' ' )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
sb . Append ( c ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
else
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
sb . Append ( @"\x" )
. Append ( ToCSharpHexDigitLower [ ( c > > 12 ) & 0x0F ] )
. Append ( ToCSharpHexDigitLower [ ( c > > 8 ) & 0x0F ] )
. Append ( ToCSharpHexDigitLower [ ( c > > 4 ) & 0x0F ] )
. Append ( ToCSharpHexDigitLower [ c & 0x0F ] ) ;
2019-05-20 17:57:28 +02:00
}
}
2022-06-07 15:28:38 +02:00
return sb . ToString ( ) ;
// requires full trust
/ *
using ( var writer = new StringWriter ( ) )
using ( var provider = CodeDomProvider . CreateProvider ( "CSharp" ) )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
provider . GenerateCodeFromExpression ( new CodePrimitiveExpression ( s ) , writer , null ) ;
return writer . ToString ( ) . Replace ( string . Format ( "\" +{0}\t\"" , Environment . NewLine ) , "" ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
* /
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static string EscapeRegexSpecialCharacters ( this string text )
{
var regexSpecialCharacters = new Dictionary < string , string >
{
{ "." , @"\." } ,
{ "(" , @"\(" } ,
{ ")" , @"\)" } ,
{ "]" , @"\]" } ,
{ "[" , @"\[" } ,
{ "{" , @"\{" } ,
{ "}" , @"\}" } ,
{ "?" , @"\?" } ,
{ "!" , @"\!" } ,
{ "$" , @"\$" } ,
{ "^" , @"\^" } ,
{ "+" , @"\+" } ,
{ "*" , @"\*" } ,
{ "|" , @"\|" } ,
{ "<" , @"\<" } ,
{ ">" , @"\>" } ,
} ;
return ReplaceMany ( text , regexSpecialCharacters ) ;
}
/// <summary>
/// Checks whether a string "haystack" contains within it any of the strings in the "needles" collection and returns
/// true if it does or false if it doesn't
/// </summary>
/// <param name="haystack">The string to check</param>
/// <param name="needles">The collection of strings to check are contained within the first string</param>
/// <param name="comparison">
/// The type of comparison to perform - defaults to <see cref="StringComparison.CurrentCulture" />
/// </param>
/// <returns>True if any of the needles are contained with haystack; otherwise returns false</returns>
/// Added fix to ensure the comparison is used - see http://issues.umbraco.org/issue/U4-11313
public static bool ContainsAny ( this string haystack , IEnumerable < string > needles , StringComparison comparison = StringComparison . CurrentCulture )
{
if ( haystack = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( "haystack" ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrEmpty ( haystack ) | | needles = = null | | ! needles . Any ( ) )
{
return false ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
return needles . Any ( value = > haystack . IndexOf ( value , comparison ) > = 0 ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
public static bool CsvContains ( this string csv , string value )
{
if ( string . IsNullOrEmpty ( csv ) )
{
return false ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
var idCheckList = csv . Split ( Constants . CharArrays . Comma , StringSplitOptions . RemoveEmptyEntries ) ;
return idCheckList . Contains ( value ) ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Converts a file name to a friendly name for a content item
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static string ToFriendlyName ( this string fileName )
{
// strip the file extension
fileName = fileName . StripFileExtension ( ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// underscores and dashes to spaces
fileName = fileName . ReplaceMany ( Constants . CharArrays . UnderscoreDash , ' ' ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// any other conversions ?
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// Pascalcase (to be done last)
fileName = CultureInfo . InvariantCulture . TextInfo . ToTitleCase ( fileName ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
// Replace multiple consecutive spaces with a single space
fileName = string . Join ( " " , fileName . Split ( Constants . CharArrays . Space , StringSplitOptions . RemoveEmptyEntries ) ) ;
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
return fileName ;
}
2019-05-20 17:57:28 +02:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// An extension method that returns a new string in which all occurrences of an
/// unicode characters that are invalid in XML files are replaced with an empty string.
/// </summary>
/// <param name="text">Current instance of the string</param>
/// <returns>Updated string</returns>
/// <summary>
/// removes any unusual unicode characters that can't be encoded into XML
/// </summary>
public static string ToValidXmlString ( this string text ) = >
string . IsNullOrEmpty ( text ) ? text : InvalidXmlChars . Value . Replace ( text , string . Empty ) ;
/// <summary>
/// Converts a string to a Guid - WARNING, depending on the string, this may not be unique
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static Guid ToGuid ( this string text ) = >
CreateGuidFromHash (
UrlNamespace ,
text ,
CryptoConfig . AllowOnlyFipsAlgorithms ? 5 // SHA1
: 3 ) ; // MD5
/// <summary>
/// Turns an null-or-whitespace string into a null string.
/// </summary>
2022-11-08 09:22:23 +01:00
public static string? NullOrWhiteSpaceAsNull ( this string? text )
2022-06-07 15:28:38 +02:00
= > string . IsNullOrWhiteSpace ( text ) ? null : text ;
/// <summary>
/// Creates a name-based UUID using the algorithm from RFC 4122 §4.3.
/// See
/// <a href="https://github.com/LogosBible/Logos.Utility/blob/master/src/Logos.Utility/GuidUtility.cs#L34">GuidUtility.cs</a>
/// for original implementation.
/// </summary>
/// <param name="namespaceId">The ID of the namespace.</param>
/// <param name="name">The name (within that namespace).</param>
/// <param name="version">
/// The version number of the UUID to create; this value must be either
/// 3 (for MD5 hashing) or 5 (for SHA-1 hashing).
/// </param>
/// <returns>A UUID derived from the namespace and name.</returns>
/// <remarks>
/// See
/// <a href="http://code.logos.com/blog/2011/04/generating_a_deterministic_guid.html">Generating a deterministic GUID</a>
/// .
/// </remarks>
internal static Guid CreateGuidFromHash ( Guid namespaceId , string name , int version )
{
if ( name = = null )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( "name" ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
if ( version ! = 3 & & version ! = 5 )
2019-05-20 17:57:28 +02:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentOutOfRangeException ( "version" , "version must be either 3 or 5." ) ;
2019-05-20 17:57:28 +02:00
}
2022-06-07 15:28:38 +02:00
// convert the name to a sequence of octets (as defined by the standard or conventions of its namespace) (step 3)
// ASSUME: UTF-8 encoding is always appropriate
var nameBytes = Encoding . UTF8 . GetBytes ( name ) ;
2019-12-05 10:41:58 +01:00
2022-06-07 15:28:38 +02:00
// convert the namespace UUID to network order (step 3)
var namespaceBytes = namespaceId . ToByteArray ( ) ;
SwapByteOrder ( namespaceBytes ) ;
2019-12-05 10:41:58 +01:00
2022-06-07 15:28:38 +02:00
// comput the hash of the name space ID concatenated with the name (step 4)
byte [ ] hash ;
using ( HashAlgorithm algorithm = version = = 3 ? MD5 . Create ( ) : SHA1 . Create ( ) )
2019-12-05 10:41:58 +01:00
{
2022-06-07 15:28:38 +02:00
algorithm . TransformBlock ( namespaceBytes , 0 , namespaceBytes . Length , null , 0 ) ;
algorithm . TransformFinalBlock ( nameBytes , 0 , nameBytes . Length ) ;
hash = algorithm . Hash ! ;
2019-12-05 10:41:58 +01:00
}
2022-06-07 15:28:38 +02:00
// most bytes from the hash are copied straight to the bytes of the new GUID (steps 5-7, 9, 11-12)
var newGuid = new byte [ 16 ] ;
Array . Copy ( hash , 0 , newGuid , 0 , 16 ) ;
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
// set the four most significant bits (bits 12 through 15) of the time_hi_and_version field to the appropriate 4-bit version number from Section 4.1.3 (step 8)
newGuid [ 6 ] = ( byte ) ( ( newGuid [ 6 ] & 0x0F ) | ( version < < 4 ) ) ;
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
// set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively (step 10)
newGuid [ 8 ] = ( byte ) ( ( newGuid [ 8 ] & 0x3F ) | 0x80 ) ;
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
// convert the resulting UUID to local byte order (step 13)
SwapByteOrder ( newGuid ) ;
return new Guid ( newGuid ) ;
}
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
// Converts a GUID (expressed as a byte array) to/from network order (MSB-first).
internal static void SwapByteOrder ( byte [ ] guid )
{
SwapBytes ( guid , 0 , 3 ) ;
SwapBytes ( guid , 1 , 2 ) ;
SwapBytes ( guid , 4 , 5 ) ;
SwapBytes ( guid , 6 , 7 ) ;
}
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
private static void SwapBytes ( byte [ ] guid , int left , int right )
{
var temp = guid [ left ] ;
guid [ left ] = guid [ right ] ;
guid [ right ] = temp ;
}
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Checks if a given path is a full path including drive letter
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
2022-09-19 10:29:12 +02:00
public static bool IsFullPath ( this string path ) = > Path . IsPathFullyQualified ( path ) ;
2022-06-07 15:28:38 +02:00
// FORMAT STRINGS
/// <summary>
/// Cleans a string to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias ( this string alias , IShortStringHelper ? shortStringHelper ) = >
shortStringHelper ? . CleanStringForSafeAlias ( alias ) ? ? string . Empty ;
/// <summary>
/// Cleans a string to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="camel">A value indicating that we want to camel-case the alias.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias ( this string alias , IShortStringHelper shortStringHelper , bool camel )
{
var a = shortStringHelper . CleanStringForSafeAlias ( alias ) ;
if ( string . IsNullOrWhiteSpace ( a ) | | camel = = false )
2019-12-20 17:36:44 +01:00
{
2022-06-07 15:28:38 +02:00
return a ;
2019-12-20 17:36:44 +01:00
}
2022-06-07 15:28:38 +02:00
return char . ToLowerInvariant ( a [ 0 ] ) + a . Substring ( 1 ) ;
}
2019-12-20 17:36:44 +01:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an alias.
/// </summary>
/// <param name="alias">The text to filter.</param>
/// <param name="culture">The culture.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe alias.</returns>
public static string ToSafeAlias ( this string alias , IShortStringHelper shortStringHelper , string culture ) = >
shortStringHelper . CleanStringForSafeAlias ( alias , culture ) ;
// the new methods to get a url segment
/// <summary>
/// Cleans a string to produce a string that can safely be used in an url segment.
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <returns>The safe url segment.</returns>
public static string ToUrlSegment ( this string text , IShortStringHelper shortStringHelper )
{
if ( text = = null )
2019-12-09 14:12:06 +01:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( text ) ) ;
2019-12-09 14:12:06 +01:00
}
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrWhiteSpace ( text ) )
2019-12-09 14:12:06 +01:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentException (
"Value can't be empty or consist only of white-space characters." ,
nameof ( text ) ) ;
2019-12-09 14:12:06 +01:00
}
2022-06-07 15:28:38 +02:00
return shortStringHelper . CleanStringForUrlSegment ( text ) ;
}
2019-12-09 14:12:06 +01:00
2022-06-07 15:28:38 +02:00
/// <summary>
/// Cleans a string, in the context of a specified culture, to produce a string that can safely be used in an url
/// segment.
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="culture">The culture.</param>
/// <returns>The safe url segment.</returns>
public static string ToUrlSegment ( this string text , IShortStringHelper shortStringHelper , string? culture )
{
if ( text = = null )
2019-12-09 14:12:06 +01:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentNullException ( nameof ( text ) ) ;
2019-12-09 14:12:06 +01:00
}
2019-12-18 18:55:00 +01:00
2022-06-07 15:28:38 +02:00
if ( string . IsNullOrWhiteSpace ( text ) )
2019-12-18 18:55:00 +01:00
{
2022-06-07 15:28:38 +02:00
throw new ArgumentException (
"Value can't be empty or consist only of white-space characters." ,
nameof ( text ) ) ;
2019-12-18 18:55:00 +01:00
}
2022-06-07 15:28:38 +02:00
return shortStringHelper . CleanStringForUrlSegment ( text , culture ) ;
}
/// <summary>
/// Cleans a string.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
public static string ToCleanString ( this string text , IShortStringHelper shortStringHelper , CleanStringType stringType ) = > shortStringHelper . CleanString ( text , stringType ) ;
/// <summary>
/// Cleans a string, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="separator">The separator.</param>
/// <returns>The clean string.</returns>
/// <remarks>The string is cleaned in the context of the ICurrent.ShortStringHelper default culture.</remarks>
public static string ToCleanString ( this string text , IShortStringHelper shortStringHelper , CleanStringType stringType , char separator ) = > shortStringHelper . CleanString ( text , stringType , separator ) ;
/// <summary>
/// Cleans a string in the context of a specified culture.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="culture">The culture.</param>
/// <returns>The clean string.</returns>
public static string ToCleanString ( this string text , IShortStringHelper shortStringHelper , CleanStringType stringType , string culture ) = > shortStringHelper . CleanString ( text , stringType , culture ) ;
/// <summary>
/// Cleans a string in the context of a specified culture, using a specified separator.
/// </summary>
/// <param name="text">The text to clean.</param>
/// <param name="shortStringHelper">The short string helper.</param>
/// <param name="stringType">
/// A flag indicating the target casing and encoding of the string. By default,
/// strings are cleaned up to camelCase and Ascii.
/// </param>
/// <param name="separator">The separator.</param>
/// <param name="culture">The culture.</param>
/// <returns>The clean string.</returns>
public static string ToCleanString ( this string text , IShortStringHelper shortStringHelper , CleanStringType stringType , char separator , string culture ) = >
shortStringHelper . CleanString ( text , stringType , separator , culture ) ;
// note: LegacyCurrent.ShortStringHelper will produce 100% backward-compatible output for SplitPascalCasing.
// other helpers may not. DefaultCurrent.ShortStringHelper produces better, but non-compatible, results.
/// <summary>
/// Splits a Pascal cased string into a phrase separated by spaces.
/// </summary>
/// <param name="phrase">The text to split.</param>
/// <param name="shortStringHelper"></param>
/// <returns>The split text.</returns>
public static string SplitPascalCasing ( this string phrase , IShortStringHelper shortStringHelper ) = >
shortStringHelper . SplitPascalCasing ( phrase , ' ' ) ;
/// <summary>
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
/// filename,
/// both internally (on disk) and externally (as a url).
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper"></param>
/// <returns>The safe filename.</returns>
public static string ToSafeFileName ( this string text , IShortStringHelper shortStringHelper ) = >
shortStringHelper . CleanStringForSafeFileName ( text ) ;
// NOTE: Not sure what this actually does but is used a few places, need to figure it out and then move to StringExtensions and obsolete.
// it basically is yet another version of SplitPascalCasing
// plugging string extensions here to be 99% compatible
// the only diff. is with numbers, Number6Is was "Number6 Is", and the new string helper does it too,
// but the legacy one does "Number6Is"... assuming it is not a big deal.
internal static string SpaceCamelCasing ( this string phrase , IShortStringHelper shortStringHelper ) = >
phrase . Length < 2 ? phrase : phrase . SplitPascalCasing ( shortStringHelper ) . ToFirstUpperInvariant ( ) ;
/// <summary>
/// Cleans a string, in the context of the invariant culture, to produce a string that can safely be used as a
/// filename,
/// both internally (on disk) and externally (as a url).
/// </summary>
/// <param name="text">The text to filter.</param>
/// <param name="shortStringHelper"></param>
/// <param name="culture">The culture.</param>
/// <returns>The safe filename.</returns>
public static string ToSafeFileName ( this string text , IShortStringHelper shortStringHelper , string culture ) = >
shortStringHelper . CleanStringForSafeFileName ( text , culture ) ;
/// <summary>
/// Splits a string with an escape character that allows for the split character to exist in a string
/// </summary>
/// <param name="value">The string to split</param>
/// <param name="splitChar">The character to split on</param>
/// <param name="escapeChar">The character which can be used to escape the character to split on</param>
/// <returns>The string split into substrings delimited by the split character</returns>
public static IEnumerable < string > EscapedSplit ( this string value , char splitChar , char escapeChar = DefaultEscapedStringEscapeChar )
{
if ( value = = null )
2019-12-18 18:55:00 +01:00
{
2022-06-07 15:28:38 +02:00
yield break ;
2019-12-18 18:55:00 +01:00
}
2022-06-07 15:28:38 +02:00
var sb = new StringBuilder ( value . Length ) ;
var escaped = false ;
2021-03-18 15:04:03 -07:00
2022-06-07 15:28:38 +02:00
foreach ( var chr in value . ToCharArray ( ) )
{
if ( escaped )
2021-03-18 15:04:03 -07:00
{
2022-06-07 15:28:38 +02:00
escaped = false ;
sb . Append ( chr ) ;
}
else if ( chr = = splitChar )
{
yield return sb . ToString ( ) ;
sb . Clear ( ) ;
}
else if ( chr = = escapeChar )
{
escaped = true ;
}
else
{
sb . Append ( chr ) ;
2021-03-18 15:04:03 -07:00
}
}
2022-06-07 15:28:38 +02:00
yield return sb . ToString ( ) ;
2019-05-20 17:57:28 +02:00
}
2023-11-14 11:16:12 +01:00
// having benchmarked various solutions (incl. for/foreach, split and LINQ based ones),
// this is by far the fastest way to find string needles in a string haystack
public static int CountOccurrences ( this string haystack , string needle )
= > haystack . Length - haystack . Replace ( needle , string . Empty ) . Length ;
2019-05-20 17:57:28 +02:00
}