wip
This commit is contained in:
parent
84b3b5c150
commit
74035d5f13
4 changed files with 938 additions and 682 deletions
|
|
@ -19,7 +19,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -31,7 +31,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -47,7 +47,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -64,7 +64,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -80,7 +80,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -94,7 +94,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -106,7 +106,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -125,7 +125,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -136,7 +136,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -148,7 +148,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Satisfies(v)).IsEqualTo(expected);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -165,7 +165,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
var json = "\"^1.2.3\"";
|
||||
var r = System.Text.Json.JsonSerializer.Deserialize<SemanticVersionRange>(json);
|
||||
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
|
||||
await Assert.That(r.Satisfies(new SemanticVersion(1, 2, 4))).IsTrue();
|
||||
await Assert.That(r.Contains(new SemanticVersion(1, 2, 4))).IsTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -200,4 +200,15 @@ internal sealed class SemanticVersionRangeTests
|
|||
await Assert.That(success).IsTrue();
|
||||
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
||||
{
|
||||
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
var destination = new char[5];
|
||||
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||
|
||||
await Assert.That(success).IsFalse();
|
||||
await Assert.That(charsWritten).IsEqualTo(0);
|
||||
}
|
||||
}
|
||||
75
src/semver/SemanticVersionRange.Formatting.cs
Normal file
75
src/semver/SemanticVersionRange.Formatting.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
if (format is not null and not "m" and not "n" and not "ns")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||
handler.AppendFormatted(this, format);
|
||||
return handler.ToStringAndClear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanFormattable
|
||||
|
||||
/// <summary>
|
||||
/// Tries to format the semantic version range into the specified span of characters.
|
||||
/// </summary>
|
||||
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||
{
|
||||
return TryFormat(destination, out charsWritten, default, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "ns";
|
||||
}
|
||||
|
||||
if (format is not "m" and not "n" and not "ns")
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var builder = new SpanStringBuilder(destination, provider);
|
||||
|
||||
if (_sets is not { Length: > 0 } sets)
|
||||
{
|
||||
builder.AppendLiteral(format is "m" ? "(,)" : "*");
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
builder.AppendLiteral(format is "m" ? "," : " || ");
|
||||
}
|
||||
|
||||
sets[i].AppendFormatted(ref builder, format);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.TryComplete(out charsWritten);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
462
src/semver/SemanticVersionRange.Parsing.cs
Normal file
462
src/semver/SemanticVersionRange.Parsing.cs
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(string s)
|
||||
{
|
||||
return Parse(s.AsSpan(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s)
|
||||
{
|
||||
return Parse(s, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var result))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryParseJava(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new JavaSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseJavaSet(part, out var set))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseJavaSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s is not ['[' or '(', .., ']' or ')'])
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(lower: (version, true), upper: null)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var inclusiveStart = s[0] is '[';
|
||||
var inclusiveEnd = s[^1] is ']';
|
||||
var text = s[1..^1];
|
||||
var comma = text.IndexOf(',');
|
||||
|
||||
if (comma < 0)
|
||||
{
|
||||
if (!inclusiveStart || !inclusiveEnd || !SemanticVersion.TryParsePartially(text.Trim(), out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparator.Equal, version)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryCreateBound(text[..comma].Trim(), inclusiveStart, out var lower) ||
|
||||
!TryCreateBound(text[(comma + 1)..].Trim(), inclusiveEnd, out var upper))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet(lower is null && upper is null ? [] : [new Constraint(lower: lower, upper: upper)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateBound(ReadOnlySpan<char> s, bool inclusive, out (SemanticVersion Version, bool Inclusive)? bound)
|
||||
{
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
bound = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out _))
|
||||
{
|
||||
bound = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bound = (version, inclusive);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ref struct JavaSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public JavaSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||
{
|
||||
_span = readOnlySpan;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var depth = 0;
|
||||
var i = _currentStart;
|
||||
|
||||
while (i < _span.Length)
|
||||
{
|
||||
var ch = _span[i];
|
||||
|
||||
if (ch is '[' or '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch is ']' or ')')
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else if (ch is ',' && depth is 0)
|
||||
{
|
||||
_currentEnd = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
_currentEnd = _span.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly JavaSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNode(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new NodeSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseNodeSet(part, out var set))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNodeSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
if (s.IndexOf(" - ") is not -1 and var hyphen)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(s[..hyphen].Trim(), out var lower))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(s[(hyphen + 3)..].Trim(), out var upper))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(lower: (lower, true), upper: (upper, true))]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var list = new List<Constraint>(2);
|
||||
var current = s;
|
||||
while (TryReadToken(ref current, out var token))
|
||||
{
|
||||
scoped ReadOnlySpan<char> instruction = [];
|
||||
|
||||
if (token is ">=" or "<=" or ">" or "<" or "~" or "^" or "=" or "!=")
|
||||
{
|
||||
instruction = token;
|
||||
|
||||
if (!TryReadToken(ref current, out token))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (token.StartsWith(">="))
|
||||
{
|
||||
instruction = ">=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith("<="))
|
||||
{
|
||||
instruction = "<=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith("!="))
|
||||
{
|
||||
instruction = "!=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith(">"))
|
||||
{
|
||||
instruction = ">";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("<"))
|
||||
{
|
||||
instruction = "<";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("^"))
|
||||
{
|
||||
instruction = "^";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("~"))
|
||||
{
|
||||
instruction = "~";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("="))
|
||||
{
|
||||
instruction = "=";
|
||||
token = token[1..];
|
||||
}
|
||||
}
|
||||
|
||||
if (token is "*" or "x" or "X")
|
||||
{
|
||||
set = new ConstraintSet([default]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryParseNpmConstraint(instruction, token.Trim(), list))
|
||||
{
|
||||
set = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
set = new ConstraintSet([.. list]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadToken(ref ReadOnlySpan<char> s, out ReadOnlySpan<char> token)
|
||||
{
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
token = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var separator = s.IndexOf(' ');
|
||||
if (separator < 0)
|
||||
{
|
||||
token = s;
|
||||
s = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
token = s[..separator];
|
||||
s = s[separator..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNpmConstraint(ReadOnlySpan<char> op, ReadOnlySpan<char> s, ICollection<Constraint> constraints)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out var components))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case "^":
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (GetCaretUpperBound(version, components), false)));
|
||||
return true;
|
||||
case "~":
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (GetTildeUpperBound(version, components), false)));
|
||||
return true;
|
||||
case ">":
|
||||
constraints.Add(new Constraint(Comparator.Greater, version));
|
||||
return true;
|
||||
case ">=":
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
return true;
|
||||
case "<":
|
||||
constraints.Add(new Constraint(Comparator.Less, version));
|
||||
return true;
|
||||
case "<=":
|
||||
constraints.Add(new Constraint(Comparator.LessOrEqual, version));
|
||||
return true;
|
||||
case "!=":
|
||||
constraints.Add(new Constraint(Comparator.NotEqual, version));
|
||||
return true;
|
||||
default:
|
||||
switch (components)
|
||||
{
|
||||
case 3:
|
||||
constraints.Add(new Constraint(Comparator.Equal, version));
|
||||
return true;
|
||||
case 2:
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major, version.Minor + 1, 0), false)));
|
||||
return true;
|
||||
case 1:
|
||||
constraints.Add(new Constraint(lower: (version, true), upper: (new SemanticVersion(version.Major + 1, 0, 0), false)));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ref struct NodeSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public NodeSetGrouping(ReadOnlySpan<char> span)
|
||||
{
|
||||
_span = span;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 2;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = _span[_currentStart..].IndexOf("||");
|
||||
_currentEnd = index >= 0 ? _currentStart + index : _span.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly NodeSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue