You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Utilities/MaskAlgorithms/RegexMask.cs

272 lines
9.3 KiB

// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Connected;
public class RegexMask : BaseMask
{
/// <summary>
/// Add this filter to the end of a mask to block any space, tab or newline character.
/// </summary>
private const string WhiteSpaceFilter = "(?!\\s)";
/// <summary>
/// Create a mask that uses a regex to restrict input.
/// </summary>
/// <param name="regex">
/// The general or progressive regex to be used for input checking.
///
/// Note: a general regex must match every possible input, i.e. ^[0-9]+$.
/// Note: a progressive regex must match even partial input successfully! The
/// progressive regex must start with ^ and end with $ to work correctly!
///
/// Example: to match input "abc" a progressive regex must match "a" or "ab" or "abc". The
/// progressive regex would look like this: ^a(b(c)?)?$ or like this ^(a|ab|abc)$
/// It is best to generate the progressive regex automatically like BlockMask does.
/// </param>
/// <param name="mask">
/// The mask defining the structure of the accepted input.
///
/// Note: if not included the regex will be the mask.
/// </param>
public RegexMask(string regex, string mask = null)
{
_regexPattern = regex;
Mask = mask ?? regex;
}
protected string _regexPattern;
protected Regex _regex;
/// <summary>
/// Optional delimiter chars which will be jumped over if the caret is
/// in front of one and the user inputs the next non-delimiter
/// </summary>
public string Delimiters { get; protected set; }
protected override void InitInternals()
{
base.InitInternals();
Delimiters ??= "";
_delimiters = new HashSet<char>(Delimiters);
InitRegex();
}
protected virtual void InitRegex()
{
_regex = new Regex(_regexPattern);
}
/// <summary>
/// Inserts given text at caret position
/// </summary>
/// <param name="input">One or multiple characters of input</param>
public override void Insert(string input)
{
Init();
DeleteSelection(align: false);
var text = Text ?? "";
var pos = ConsolidateCaret(text, CaretPos);
(var beforeText, var afterText) = SplitAt(text, pos);
var alignedInput = AlignAgainstMask(beforeText + input);
CaretPos = alignedInput.Length;
UpdateText(AlignAgainstMask(alignedInput + afterText));
}
protected override void DeleteSelection(bool align)
{
ConsolidateSelection();
if (Selection == null)
return;
var sel = Selection.Value;
(var s1, _, var s3) = SplitSelection(Text, sel);
Selection = null;
CaretPos = sel.Item1;
if (!align)
UpdateText(s1 + s3);
else
UpdateText(AlignAgainstMask(s1 + s3));
}
/// <summary>
/// Implements the effect of the Del key at the current cursor position
/// </summary>
public override void Delete()
{
Init();
if (Selection != null)
{
DeleteSelection(align: true);
return;
}
var text = Text ?? "";
var pos = ConsolidateCaret(text, CaretPos);
if (pos >= text.Length)
return;
(var beforeText, var afterText) = SplitAt(text, pos);
// delete as many delimiters as there are plus one char
var restText = new string(afterText.SkipWhile(IsDelimiter).Skip(1).ToArray());
UpdateText( AlignAgainstMask(beforeText + restText));
var numDeleted = afterText.Length - restText.Length;
if (numDeleted > 1)
{
// since we just auto-deleted delimiters which were re-created by AlignAgainstMask we can just as well
// adjust the cursor position to after the delimiters
CaretPos += (numDeleted - 1);
}
}
/// <summary>
/// Implements the effect of the Backspace key at the current cursor position
/// </summary>
public override void Backspace()
{
Init();
if (Selection != null)
{
DeleteSelection(align: true);
return;
}
var text = Text ?? "";
var pos = ConsolidateCaret(text, CaretPos);
if (pos == 0)
return;
(var beforeText, var afterText) = SplitAt(text, pos);
// backspace as many delimiters as there are plus one char
var restText = new string(beforeText.Reverse().SkipWhile(IsDelimiter).Skip(1).Reverse().ToArray());
var numDeleted = beforeText.Length - restText.Length;
CaretPos -= numDeleted;
UpdateText(AlignAgainstMask(restText + afterText));
}
/// <summary>
/// Applies the mask to the given text starting at the given offset and returns the masked text.
/// </summary>
/// <param name="text"></param>
protected virtual string AlignAgainstMask(string text)
{
text ??= "";
var alignedText = "";
var textIndex = 0; // index in text
while (textIndex < text.Length)
{
var textChar = text[textIndex];
if (_regex.IsMatch(alignedText + textChar))
alignedText += textChar;
// try to skip over a delimiter (input of values only i.e. 31122021 => 31.12.2021)
else if (Delimiters.Length > 0)
{
foreach (var d in Delimiters)
{
if (_regex.IsMatch(alignedText + d + textChar))
{
alignedText += (d.ToString() + textChar);
break;
}
}
}
textIndex++;
}
return alignedText;
}
public override void UpdateFrom(IMask other)
{
base.UpdateFrom(other);
var o = other as RegexMask;
if (o == null)
return;
if (Delimiters != o.Delimiters)
{
Delimiters = o.Delimiters;
_initialized = false;
}
Refresh();
}
/// <summary>
/// Creates a predefined RegexMask for an IPv4 Address with or without port masking.
/// </summary>
/// <param name="includePort">
/// Set to true to include port to the mask.
/// </param>
/// <param name="maskChar">
/// Set the IPv4 maskChar. Default is '0'
/// </param>
public static RegexMask IPv4(bool includePort = false, char maskChar = '0')
{
const string Octet = "25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{0,2}";
var ipv4 = $"(?:{Octet})(?:\\.(?:{Octet})){{0,3}}";
var delimiters = ".";
var octetMask = new string(maskChar, 3);
var mask = string.Join(delimiters, Enumerable.Repeat(octetMask, 4));
if (includePort)
{
const string IpPort =
"(:|:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?";
ipv4 = $"{ipv4}{IpPort}";
mask = $"{mask}:{new string(maskChar, 5)}";
delimiters += ":";
}
var regex = $"^{ipv4}{WhiteSpaceFilter}$";
var regexMask = new RegexMask(regex, mask) { Delimiters = delimiters };
return regexMask;
}
/// <summary>
/// Creates a predefined RegexMask for an IPv6 Address with or without port masking.
/// </summary>
/// <param name="includePort">
/// Set to true to include port to the mask.
/// </param>
/// <param name="maskChar">
/// Set the IPv6 maskChar. Default is 'X'
/// </param>
/// <param name="portMaskChar">
/// Set the IPv6 portMask. Default is '0'
/// </param>
public static RegexMask IPv6(bool includePort = false, char maskChar = 'X', char portMaskChar = '0')
{
const string Hex = "[0-9A-Fa-f]{0,4}";
const string IPv6Filter = "(?!.*?[:]{2}?:)";
var ipv6 = $"{Hex}(:{Hex}){{0,7}}";
var delimiters = ":";
var hexMask = new string(maskChar, 4);
var mask = string.Join(delimiters, Enumerable.Repeat(hexMask, 8));
if (includePort)
{
const string IpPort =
"(\\]|\\]:|\\]:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?";
ipv6 = $"((\\[{ipv6}){IpPort})";
mask = $"[{mask}]:{new(portMaskChar, 5)}";
delimiters += "[]";
}
var regex = $"^{IPv6Filter}{ipv6}{WhiteSpaceFilter}$";
var regexMask = new RegexMask(regex, mask) { Delimiters = delimiters, AllowOnlyDelimiters = true };
return regexMask;
}
/// <summary>
/// Creates a predefined RegexMask for Email Address.
/// </summary>
/// <param name="mask">
/// Set the email mask. Default is "Ex. user@domain.com"
/// </param>
public static RegexMask Email(string mask = "Ex. user@domain.com")
{
const string Regex = $"^(?>[\\w\\-\\+]+\\.?)+(?>@?|@)(?<!(\\.@))(?>\\w+\\.)*(\\w+)?{WhiteSpaceFilter}$";
const string Delimiters = "@.";
var regexMask = new RegexMask(Regex, mask) { Delimiters = Delimiters };
return regexMask;
}
}