// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System;
namespace SixLabors.Fonts.Unicode {
///
/// An enumerator for retrieving word-boundary segments from a .
///
/// Implements the Unicode Word Boundary Algorithm. UAX #29
///
///
/// Methods are pattern-matched by compiler to allow using foreach pattern.
///
public ref struct SpanWordEnumerator
{
private ReadOnlySpan source;
private int sourceOffset;
private int codePointOffset;
///
/// Initializes a new instance of the struct.
///
/// The buffer to read from.
public SpanWordEnumerator(ReadOnlySpan source)
{
this.source = source;
this.sourceOffset = 0;
this.codePointOffset = 0;
this.Current = default;
}
///
/// Gets the element in the collection at the current position of the enumerator.
///
public WordSegment Current { get; private set; }
///
/// Returns an enumerator that iterates through the collection.
///
/// An enumerator that iterates through the collection.
public readonly SpanWordEnumerator GetEnumerator() => this;
///
/// Advances the enumerator to the next element of the collection.
///
///
/// if the enumerator was successfully advanced to the next element;
/// if the enumerator has passed the end of the collection.
///
public bool MoveNext()
{
if (this.sourceOffset >= this.source.Length)
{
return false;
}
int segmentStart = this.sourceOffset;
int segmentCodePointStart = this.codePointOffset;
WordBreakCodePoint current = this.ReadForward(this.sourceOffset);
int currentEnd = current.Utf16End;
int boundaryCodePoint = this.codePointOffset + 1;
while (currentEnd < this.source.Length)
{
WordBreakCodePoint next = this.ReadForward(currentEnd);
if (this.IsBoundary(current, next))
{
break;
}
current = next;
currentEnd = current.Utf16End;
boundaryCodePoint++;
}
this.Current = new WordSegment(
this.source[segmentStart..currentEnd],
segmentStart,
segmentCodePointStart,
boundaryCodePoint - segmentCodePointStart);
this.sourceOffset = currentEnd;
this.codePointOffset = boundaryCodePoint;
return true;
}
private readonly bool IsBoundary(in WordBreakCodePoint current, in WordBreakCodePoint next)
{
// WB3, WB3a, WB3b, and WB3c are evaluated before the ignore rule, so they
// use the adjacent code points exactly as they appear in the source.
if (current.Is(WordBreakClass.CarriageReturn) && next.Is(WordBreakClass.LineFeed))
{
return false;
}
if (IsNewline(current) || IsNewline(next))
{
return true;
}
if (current.Is(WordBreakClass.ZeroWidthJoiner)
&& CodePoint.GetGraphemeClusterClass(next.CodePoint) == GraphemeClusterClass.ExtendedPictographic)
{
return false;
}
if (current.Is(WordBreakClass.WSegSpace) && next.Is(WordBreakClass.WSegSpace))
{
return false;
}
if (IsIgnored(next))
{
return false;
}
WordBreakCodePoint left = this.GetEffectivePrevious(current);
WordBreakClass right = next.Class;
if (IsAHLetter(left.Class) && IsAHLetter(right))
{
return false;
}
if (IsAHLetter(left.Class)
&& IsMidLetterMidNumLetQ(right)
&& this.TryGetNextSignificant(next.Utf16End, out WordBreakCodePoint after)
&& IsAHLetter(after.Class))
{
return false;
}
if (IsAHLetter(right)
&& IsMidLetterMidNumLetQ(left.Class)
&& this.TryGetPreviousSignificant(left.Utf16Start, out WordBreakCodePoint before)
&& IsAHLetter(before.Class))
{
return false;
}
if (left.Is(WordBreakClass.HebrewLetter) && right == WordBreakClass.SingleQuote)
{
return false;
}
if (left.Is(WordBreakClass.HebrewLetter)
&& right == WordBreakClass.DoubleQuote
&& this.TryGetNextSignificant(next.Utf16End, out after)
&& after.Is(WordBreakClass.HebrewLetter))
{
return false;
}
if (right == WordBreakClass.HebrewLetter
&& left.Is(WordBreakClass.DoubleQuote)
&& this.TryGetPreviousSignificant(left.Utf16Start, out before)
&& before.Is(WordBreakClass.HebrewLetter))
{
return false;
}
if (left.Is(WordBreakClass.Numeric) && right == WordBreakClass.Numeric)
{
return false;
}
if (IsAHLetter(left.Class) && right == WordBreakClass.Numeric)
{
return false;
}
if (left.Is(WordBreakClass.Numeric) && IsAHLetter(right))
{
return false;
}
if (right == WordBreakClass.Numeric
&& IsMidNumMidNumLetQ(left.Class)
&& this.TryGetPreviousSignificant(left.Utf16Start, out before)
&& before.Is(WordBreakClass.Numeric))
{
return false;
}
if (left.Is(WordBreakClass.Numeric)
&& IsMidNumMidNumLetQ(right)
&& this.TryGetNextSignificant(next.Utf16End, out after)
&& after.Is(WordBreakClass.Numeric))
{
return false;
}
if (left.Is(WordBreakClass.Katakana) && right == WordBreakClass.Katakana)
{
return false;
}
if (IsAHLetterNumericKatakanaExtendNumLet(left.Class) && right == WordBreakClass.ExtendNumLet)
{
return false;
}
if (left.Is(WordBreakClass.ExtendNumLet) && IsAHLetterNumericKatakana(right))
{
return false;
}
if (left.Is(WordBreakClass.RegionalIndicator)
&& right == WordBreakClass.RegionalIndicator
&& (this.CountRegionalIndicatorsBefore(next.Utf16Start) & 1) == 1)
{
return false;
}
return true;
}
private readonly WordBreakCodePoint GetEffectivePrevious(in WordBreakCodePoint current)
{
if (!IsIgnored(current))
{
return current;
}
int scanEnd = current.Utf16Start;
while (this.TryReadBackward(scanEnd, out WordBreakCodePoint previous))
{
if (!IsIgnored(previous))
{
// WB4 deliberately stops ignoring after sot and hard line breaks.
return IsNewline(previous) ? current : previous;
}
scanEnd = previous.Utf16Start;
}
return current;
}
private readonly int CountRegionalIndicatorsBefore(int utf16End)
{
int count = 0;
int scanEnd = utf16End;
while (this.TryGetPreviousSignificant(scanEnd, out WordBreakCodePoint previous))
{
if (!previous.Is(WordBreakClass.RegionalIndicator))
{
break;
}
count++;
scanEnd = previous.Utf16Start;
}
return count;
}
private readonly bool TryGetPreviousSignificant(int utf16End, out WordBreakCodePoint codePoint)
{
int scanEnd = utf16End;
while (this.TryReadBackward(scanEnd, out codePoint))
{
if (!IsIgnored(codePoint))
{
return true;
}
scanEnd = codePoint.Utf16Start;
}
codePoint = default;
return false;
}
private readonly bool TryGetNextSignificant(int utf16Start, out WordBreakCodePoint codePoint)
{
int scanStart = utf16Start;
while (this.TryReadForward(scanStart, out codePoint))
{
if (!IsIgnored(codePoint))
{
return true;
}
scanStart = codePoint.Utf16End;
}
codePoint = default;
return false;
}
private readonly WordBreakCodePoint ReadForward(int utf16Start)
{
CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, utf16Start, out int charsConsumed);
return new WordBreakCodePoint(codePoint, CodePoint.GetWordBreakClass(codePoint), utf16Start, utf16Start + charsConsumed);
}
private readonly bool TryReadForward(int utf16Start, out WordBreakCodePoint codePoint)
{
if (utf16Start >= this.source.Length)
{
codePoint = default;
return false;
}
codePoint = this.ReadForward(utf16Start);
return true;
}
private readonly bool TryReadBackward(int utf16End, out WordBreakCodePoint codePoint)
{
if (utf16End <= 0)
{
codePoint = default;
return false;
}
int utf16Start = utf16End - 1;
if (utf16Start > 0
&& char.IsLowSurrogate(this.source[utf16Start])
&& char.IsHighSurrogate(this.source[utf16Start - 1]))
{
utf16Start--;
}
codePoint = this.ReadForward(utf16Start);
return true;
}
private static bool IsAHLetter(WordBreakClass cls)
=> cls is WordBreakClass.ALetter or WordBreakClass.HebrewLetter;
private static bool IsAHLetterNumericKatakana(WordBreakClass cls)
=> IsAHLetter(cls) || cls is WordBreakClass.Numeric or WordBreakClass.Katakana;
private static bool IsAHLetterNumericKatakanaExtendNumLet(WordBreakClass cls)
=> IsAHLetterNumericKatakana(cls) || cls == WordBreakClass.ExtendNumLet;
private static bool IsIgnored(in WordBreakCodePoint codePoint) => IsIgnored(codePoint.Class);
private static bool IsIgnored(WordBreakClass cls)
=> cls is WordBreakClass.Extend or WordBreakClass.Format or WordBreakClass.ZeroWidthJoiner;
private static bool IsMidLetterMidNumLetQ(WordBreakClass cls)
=> cls is WordBreakClass.MidLetter or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote;
private static bool IsMidNumMidNumLetQ(WordBreakClass cls)
=> cls is WordBreakClass.MidNum or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote;
private static bool IsNewline(in WordBreakCodePoint codePoint)
=> codePoint.Class is WordBreakClass.CarriageReturn or WordBreakClass.LineFeed or WordBreakClass.Newline;
private readonly struct WordBreakCodePoint
{
public WordBreakCodePoint(CodePoint codePoint, WordBreakClass cls, int utf16Start, int utf16End)
{
this.CodePoint = codePoint;
this.Class = cls;
this.Utf16Start = utf16Start;
this.Utf16End = utf16End;
}
public CodePoint CodePoint { get; }
public WordBreakClass Class { get; }
public int Utf16Start { get; }
public int Utf16End { get; }
public bool Is(WordBreakClass cls) => this.Class == cls;
}
}
}