/// <param name="exclusiveChildBound">The exclusive upper bound on the child index to iterate to.</param>
/// <param name="nodesConsumed">How many nodes make up the sequence, if any.</param>
/// <param name="caseInsensitiveString">The string to use for an ordinal case-insensitive comparison, if any.</param>
+ /// <param name="consumeZeroWidthNodes">
+ /// Defaults to false. When false, the consumer needs the semantics of matching the produced string to fully represent
+ /// the semantics of all the consumed nodes, which means nodes can be consumed iff they produce text that's represented
+ /// by the resulting string. When true, the resulting string needs to fully represent all valid matches at that position,
+ /// but it can have false positives, which means the resulting string doesn't need to fully represent all zero-width nodes
+ /// consumed. true is only valid when used as part of a search to determine where to try a full match, not as part of
+ /// actual matching logic.
+ /// </param>
/// <returns>true if a sequence was found; otherwise, false.</returns>
- public bool TryGetOrdinalCaseInsensitiveString(int childIndex, int exclusiveChildBound, out int nodesConsumed, [NotNullWhen(true)] out string? caseInsensitiveString)
+ public bool TryGetOrdinalCaseInsensitiveString(int childIndex, int exclusiveChildBound, out int nodesConsumed, [NotNullWhen(true)] out string? caseInsensitiveString, bool consumeZeroWidthNodes = false)
{
Debug.Assert(Kind == RegexNodeKind.Concatenate, $"Expected Concatenate, got {Kind}");
vsb.Append((char)(twoChars[0] | 0x20), child.Kind is RegexNodeKind.Set ? 1 : child.M);
}
+ else if (child.Kind is RegexNodeKind.Empty)
+ {
+ // Skip over empty nodes, as they're pure nops. They would ideally have been optimized away,
+ // but can still remain in some situations.
+ }
+ else if (consumeZeroWidthNodes &&
+ // anchors
+ child.Kind is RegexNodeKind.Beginning or
+ RegexNodeKind.Bol or
+ RegexNodeKind.Start or
+ // boundaries
+ RegexNodeKind.Boundary or
+ RegexNodeKind.ECMABoundary or
+ RegexNodeKind.NonBoundary or
+ RegexNodeKind.NonECMABoundary or
+ // lookarounds
+ RegexNodeKind.NegativeLookaround or
+ RegexNodeKind.PositiveLookaround or
+ // logic
+ RegexNodeKind.UpdateBumpalong)
+ {
+ // Skip over zero-width nodes that might be reasonable at the beginning of or within a substring.
+ // We can only do these if consumeZeroWidthNodes is true, as otherwise we'd be producing a string that
+ // may not fully represent the semantics of this portion of the pattern.
+ }
else
{
break;
[InlineData(@"ab|(abc)|(abcd)", (int)RegexOptions.RightToLeft, (int)FindNextStartingPositionMode.LeadingString_RightToLeft, "ab")]
[InlineData(@"ab(?=cd)", 0, (int)FindNextStartingPositionMode.LeadingString_LeftToRight, "ab")]
[InlineData(@"ab(?=cd)", (int)RegexOptions.RightToLeft, (int)FindNextStartingPositionMode.LeadingString_RightToLeft, "ab")]
+ [InlineData(@"\bab(?=\w)(?!=\d)c\b", 0, (int)FindNextStartingPositionMode.LeadingString_LeftToRight, "abc")]
+ [InlineData(@"\bab(?=\w)(?!=\d)c\b", (int)RegexOptions.IgnoreCase, (int)FindNextStartingPositionMode.LeadingString_OrdinalIgnoreCase_LeftToRight, "abc")]
public void LeadingPrefix(string pattern, int options, int expectedMode, string expectedPrefix)
{
RegexFindOptimizations opts = ComputeOptimizations(pattern, (RegexOptions)options);
[InlineData(@"[Aa]", (int)RegexOptions.RightToLeft, (int)FindNextStartingPositionMode.LeadingSet_RightToLeft, "Aa")]
[InlineData(@"a", (int)RegexOptions.IgnoreCase | (int)RegexOptions.RightToLeft, (int)FindNextStartingPositionMode.LeadingSet_RightToLeft, "Aa")]
[InlineData(@"ab|cd|ef|gh", (int)RegexOptions.RightToLeft, (int)FindNextStartingPositionMode.LeadingSet_RightToLeft, "bdfh")]
+ [InlineData(@"\bab(?=\w)(?!=\d)c\b", (int)(RegexOptions.IgnoreCase | RegexOptions.RightToLeft), (int)FindNextStartingPositionMode.LeadingSet_RightToLeft, "Cc")]
public void LeadingSet(string pattern, int options, int expectedMode, string expectedChars)
{
RegexFindOptimizations opts = ComputeOptimizations(pattern, (RegexOptions)options);