// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Globalization; using System.Runtime.CompilerServices; namespace SixLabors.Fonts.Unicode { /// /// Enumerates potential line break opportunities for a span of text. /// This is the engine behind the Unicode Line Breaking Algorithm as defined by /// Unicode Standard Annex #14 (UAX #14): /// . /// The implementation keeps a three-code-point window over the input and applies /// the LB rules in specification order. Each rule method is named after the /// corresponding UAX #14 rule so the code can be reviewed against the standard. /// internal ref struct LineBreakEnumerator { /// /// Sentinel value representing start of text for LB2 and context checks. /// private const int StartOfText = -1; /// /// Sentinel value representing end of text for LB3. /// private const int EndOfText = -2; /// /// U+25CC DOTTED CIRCLE. LB28a treats it as an aksara base for Indic conjunct handling. /// private const int DottedCircle = 0x25CC; /// /// U+002F SOLIDUS. Used by layout-level URL tailoring around slash-separated path segments. /// private const int Solidus = 0x002F; /// /// U+003A COLON. Used to recognize URI scheme markers. /// private const int Colon = 0x003A; /// /// U+002E FULL STOP. Used to recognize www. host prefixes and URI scheme characters. /// private const int FullStop = 0x002E; /// /// U+002D HYPHEN-MINUS. Valid inside URI schemes and host labels. /// private const int HyphenMinus = 0x002D; /// /// U+00AD SOFT HYPHEN. It creates a manual hyphenation opportunity but is not rendered unless that break is chosen. /// private const int SoftHyphen = 0x00AD; /// /// U+002B PLUS SIGN. Valid inside URI schemes. /// private const int PlusSign = 0x002B; /// /// U+0057 LATIN CAPITAL LETTER W. Used by the ASCII www. recognizer. /// private const int UppercaseW = 0x0057; /// /// U+0077 LATIN SMALL LETTER W. Used by the ASCII www. recognizer. /// private const int LowercaseW = 0x0077; /// /// U+0022 QUOTATION MARK. Treated as a hard boundary while recognizing URL-like runs. /// private const int QuotationMark = 0x0022; /// /// U+0027 APOSTROPHE. Treated as a hard boundary while recognizing URL-like runs. /// private const int Apostrophe = 0x0027; /// /// U+003C LESS-THAN SIGN. Treated as a hard boundary while recognizing URL-like runs. /// private const int LessThanSign = 0x003C; /// /// U+003E GREATER-THAN SIGN. Treated as a hard boundary while recognizing URL-like runs. /// private const int GreaterThanSign = 0x003E; /// /// The UTF-16 source being inspected. The ref struct keeps this span without allocating. /// private readonly ReadOnlySpan source; /// /// Enables layout-only URL tailoring. The public constructor leaves this disabled so Unicode /// conformance tests see the un-tailored UAX #14 result. /// private readonly bool tailorUrls; /// /// UTF-16 offset of the next code point to decode from . /// private int charPosition; /// /// Code point index immediately after the last decoded code point. /// private int pointPosition; /// /// Tracks whether the artificial end-of-text sentinel has been pushed into the rule window. /// private bool endOfTextPushed; /// /// The last emitted wrap position. This prevents LB3 from emitting a duplicate final break. /// private int previousBreakPosition; /// /// The code point immediately before in the rule window. /// private LineBreakCodePoint previous; /// /// The left side of the boundary currently being evaluated. /// private LineBreakCodePoint current; /// /// The right side of the boundary currently being evaluated. /// private LineBreakCodePoint next; /// /// State for LB8. A zero width space followed by spaces permits a break after the space run. /// private bool lb8; /// /// State shared by rules that suppress breaks across a following run of spaces. /// private bool spaces; /// /// Count of consecutive regional-indicator pairs used by LB30a. /// private int regionalIndicatorCount; /// /// Streaming recognizer state used only when is enabled. /// private UrlTailoringState urlTailoringState; /// /// Initializes a new instance of the struct. /// /// The source text to inspect for UAX #14 break opportunities. public LineBreakEnumerator(ReadOnlySpan source) : this(source, false) { } /// /// Initializes a new instance of the struct. /// /// The source text to inspect for line break opportunities. /// Whether to apply layout-level URL solidus tailoring. internal LineBreakEnumerator(ReadOnlySpan source, bool tailorUrls) : this() { this.source = source; this.tailorUrls = tailorUrls; this.previous = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); this.current = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); this.next = LineBreakCodePoint.CreateSentinel(StartOfText, 0, 0); } private enum BreakAction { /// /// The rule did not apply; continue evaluating later rules. /// Pass, /// /// The rule forbids a break at the current boundary. /// NoBreak, /// /// The rule permits an optional break at the current boundary. /// MayBreak, /// /// The rule requires a break at the current boundary. /// MustBreak } /// /// Gets the most recently discovered line break opportunity. /// public LineBreak Current { get; private set; } /// /// Returns an enumerator that iterates through the collection. /// /// An enumerator that iterates through the collection. public readonly LineBreakEnumerator 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() { while (true) { if (this.charPosition < this.source.Length) { this.Push(this.ReadNext()); } else if (!this.endOfTextPushed) { this.Push(LineBreakCodePoint.CreateSentinel(EndOfText, this.next.Length, this.next.CharEnd)); this.endOfTextPushed = true; } else { this.Current = default; return false; } BreakAction action = this.GetBreakAction(); if (this.tailorUrls) { action = this.ApplyUrlTailoring(action); } switch (action) { case BreakAction.NoBreak: case BreakAction.Pass: break; case BreakAction.MayBreak: case BreakAction.MustBreak: this.Current = new LineBreak( this.FindPriorNonWhitespace(this.current), this.current.Length, action == BreakAction.MustBreak, this.current.HasValue(SoftHyphen)); this.previousBreakPosition = this.current.Length; return true; default: throw new InvalidOperationException($"Invalid line break action {action}."); } } } /// /// Decodes the next UTF-16 code point, maps its line break class according to LB1, /// and packages the additional context needed by later rules. /// private LineBreakCodePoint ReadNext() { int charStart = this.charPosition; CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, charStart, out int charsConsumed); UnicodeCategory category = CodePoint.GetGeneralCategory(codePoint); LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(codePoint), category); bool isUrlLikeRun = this.tailorUrls && this.urlTailoringState.Update(codePoint); this.charPosition += charsConsumed; this.pointPosition++; return new LineBreakCodePoint( codePoint, cls, category, this.pointPosition, charStart, this.charPosition, isUrlLikeRun); } /// /// Applies the LB1 class remapping required before any rule decisions are made. /// /// /// LB1 resolves ambiguous, surrogate, unknown, complex-context, and conditional Japanese /// starter classes before the rest of the rule chain observes them: /// AI/SG/XX to AL, SA to CM or AL based on general category, and CJ to NS. /// private static LineBreakClass MapClass(LineBreakClass c, UnicodeCategory category) => c switch { LineBreakClass.Ambiguous or LineBreakClass.Surrogate or LineBreakClass.Unknown => LineBreakClass.Alphabetic, LineBreakClass.ComplexContext => category is UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark ? LineBreakClass.CombiningMark : LineBreakClass.Alphabetic, LineBreakClass.ConditionalJapaneseStarter => LineBreakClass.Nonstarter, _ => c }; /// /// Applies the layout URL tailoring from UAX #14 section 8 while preserving the default /// enumerator behavior for callers that need strict Unicode conformance. /// /// /// The tailoring suppresses ordinary layout breaks at a solidus unless the current token has /// already been recognized as URL-like. It also adds the URL numeric path case that default /// LB25 intentionally blocks, for example the boundary after 2024/ in /// https://example/2024/05. /// private readonly BreakAction ApplyUrlTailoring(BreakAction action) { if (action == BreakAction.MustBreak || this.current.IsSentinel) { return action; } if (this.next.HasValue(Solidus)) { return BreakAction.NoBreak; } if (!this.current.HasValue(Solidus)) { return action; } if (!this.current.IsUrlLikeRun) { return BreakAction.NoBreak; } if (action == BreakAction.NoBreak && !this.previous.IsSentinel && !this.next.IsSentinel && CodePoint.IsDigit(this.previous.CodePoint) && CodePoint.IsDigit(this.next.CodePoint)) { return BreakAction.MayBreak; } return action; } /// /// Advances the three-code-point rule window. Ignored combining marks and zero width joiners /// from LB9 are folded into the current position instead of becoming a new boundary. /// private void Push(LineBreakCodePoint codePoint) { if (this.next.Ignored) { this.current.Length = this.next.Length; this.current.CharEnd = this.next.CharEnd; } else { this.previous = this.current; this.current = this.next; } this.next = codePoint; } /// /// Evaluates the UAX #14 rules in order for the boundary between /// and . /// /// /// The first rule to return anything other than decides the /// boundary. If no rule prevents a break, LB31 is represented by the final /// return. /// private BreakAction GetBreakAction() { BreakAction action; action = this.LB02(); if (action != BreakAction.Pass) { return action; } action = this.LB03(); if (action != BreakAction.Pass) { return action; } action = this.LB04(); if (action != BreakAction.Pass) { return action; } action = this.LB05(); if (action != BreakAction.Pass) { return action; } action = this.LB06(); if (action != BreakAction.Pass) { return action; } action = this.LBSpacesStop(); if (action != BreakAction.Pass) { return action; } action = this.LB07(); if (action != BreakAction.Pass) { return action; } action = this.LB08(); if (action != BreakAction.Pass) { return action; } action = this.LB08a(); if (action != BreakAction.Pass) { return action; } action = this.LB09(); if (action != BreakAction.Pass) { return action; } this.LB10(); action = this.LB11(); if (action != BreakAction.Pass) { return action; } action = this.LB12(); if (action != BreakAction.Pass) { return action; } action = this.LB12a(); if (action != BreakAction.Pass) { return action; } action = this.LB13(); if (action != BreakAction.Pass) { return action; } action = this.LB14(); if (action != BreakAction.Pass) { return action; } action = this.LB15a(); if (action != BreakAction.Pass) { return action; } action = this.LB15b(); if (action != BreakAction.Pass) { return action; } action = this.LB15c(); if (action != BreakAction.Pass) { return action; } action = this.LB15d(); if (action != BreakAction.Pass) { return action; } action = this.LB16(); if (action != BreakAction.Pass) { return action; } action = this.LB17(); if (action != BreakAction.Pass) { return action; } action = this.LB18(); if (action != BreakAction.Pass) { return action; } action = this.LB19(); if (action != BreakAction.Pass) { return action; } action = this.LB19a(); if (action != BreakAction.Pass) { return action; } action = this.LB20(); if (action != BreakAction.Pass) { return action; } action = this.LB20a(); if (action != BreakAction.Pass) { return action; } action = this.LB21a(); if (action != BreakAction.Pass) { return action; } action = this.LB21(); if (action != BreakAction.Pass) { return action; } action = this.LB21b(); if (action != BreakAction.Pass) { return action; } action = this.LB22(); if (action != BreakAction.Pass) { return action; } action = this.LB23(); if (action != BreakAction.Pass) { return action; } action = this.LB23a(); if (action != BreakAction.Pass) { return action; } action = this.LB24(); if (action != BreakAction.Pass) { return action; } action = this.LB25(); if (action != BreakAction.Pass) { return action; } action = this.LB26(); if (action != BreakAction.Pass) { return action; } action = this.LB27(); if (action != BreakAction.Pass) { return action; } action = this.LB28(); if (action != BreakAction.Pass) { return action; } action = this.LB28a(); if (action != BreakAction.Pass) { return action; } action = this.LB29(); if (action != BreakAction.Pass) { return action; } action = this.LB30(); if (action != BreakAction.Pass) { return action; } action = this.LB30a(); if (action != BreakAction.Pass) { return action; } action = this.LB30b(); if (action != BreakAction.Pass) { return action; } // LB31: Break everywhere else. return BreakAction.MayBreak; } /// /// LB2: Never break at the start of text. /// private readonly BreakAction LB02() => this.current.IsStartOfText && !this.next.IsEndOfText ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB3: Always break at the end of text. /// private readonly BreakAction LB03() => this.next.IsEndOfText && (this.current.Length == 0 || this.current.Length != this.previousBreakPosition) ? BreakAction.MayBreak : BreakAction.Pass; /// /// LB4: Always break after a mandatory break character. /// private readonly BreakAction LB04() => this.current.Is(LineBreakClass.MandatoryBreak) ? BreakAction.MustBreak : BreakAction.Pass; /// /// LB5: Treat CR followed by LF as an indivisible newline; otherwise break after CR, LF, and NL. /// private readonly BreakAction LB05() { if (this.current.Is(LineBreakClass.CarriageReturn)) { return this.next.Is(LineBreakClass.LineFeed) ? BreakAction.NoBreak : BreakAction.MustBreak; } return this.current.Is(LineBreakClass.LineFeed) || this.current.Is(LineBreakClass.NextLine) ? BreakAction.MustBreak : BreakAction.Pass; } /// /// LB6: Do not break before mandatory break characters. /// private readonly BreakAction LB06() => this.next.Is(LineBreakClass.MandatoryBreak) || this.next.Is(LineBreakClass.CarriageReturn) || this.next.Is(LineBreakClass.LineFeed) || this.next.Is(LineBreakClass.NextLine) ? BreakAction.NoBreak : BreakAction.Pass; /// /// Internal space-run handling for rules that suppress a break until after spaces have been consumed. /// /// /// This is not a standalone UAX rule. It carries the "do not break inside the intervening spaces" /// part of LB8, LB14, LB15a, LB16, and LB17 after the rule that started the space run has fired. /// It also resets the LB30a regional-indicator count whenever the current code point is not RI. /// private BreakAction LBSpacesStop() { if (!this.current.Is(LineBreakClass.RegionalIndicator)) { this.regionalIndicatorCount = 0; } if (this.spaces) { if (!this.next.Is(LineBreakClass.Space)) { this.spaces = false; } return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB7: Do not break before spaces or zero width space. /// /// /// The exceptions for ZW, OP, QU, CL, CP, and B2 are handled by their later dedicated rules. /// private readonly BreakAction LB07() { if (this.next.Is(LineBreakClass.ZeroWidthSpace)) { return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.Space) && !this.current.Is(LineBreakClass.ZeroWidthSpace) && !this.current.Is(LineBreakClass.OpenPunctuation) && !this.current.Is(LineBreakClass.Quotation) && !this.current.Is(LineBreakClass.ClosePunctuation) && !this.current.Is(LineBreakClass.CloseParenthesis) && !this.current.Is(LineBreakClass.BreakBeforeAndAfter)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB8: Break before any character following a zero width space, even if spaces intervene. /// private BreakAction LB08() { if (this.lb8) { this.lb8 = false; return BreakAction.MayBreak; } if (this.current.Is(LineBreakClass.ZeroWidthSpace)) { if (this.next.Is(LineBreakClass.Space)) { this.lb8 = true; return BreakAction.NoBreak; } return BreakAction.MayBreak; } return BreakAction.Pass; } /// /// LB8a: Do not break after a zero width joiner. /// private readonly BreakAction LB08a() => this.current.Is(LineBreakClass.ZeroWidthJoiner) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB9: Do not break a combining mark or zero width joiner away from its base character. /// /// /// When LB9 applies, the right-side code point is marked as ignored so /// folds it into the current logical position and later boundaries see the combined item. /// private BreakAction LB09() { if (!IsBkCrLfNlSpZw(this.current) && (this.next.Is(LineBreakClass.CombiningMark) || this.next.Is(LineBreakClass.ZeroWidthJoiner))) { this.next.Ignored = true; return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB10: Treat any remaining combining marks or zero width joiners as alphabetic. /// /// /// LB10 is a class rewrite rather than a boundary decision, so it does not return a /// . /// private void LB10() { if (this.current.Is(LineBreakClass.CombiningMark) || this.current.Is(LineBreakClass.ZeroWidthJoiner)) { this.current.Class = LineBreakClass.Alphabetic; } if (this.next.Is(LineBreakClass.CombiningMark) || this.next.Is(LineBreakClass.ZeroWidthJoiner)) { this.next.Class = LineBreakClass.Alphabetic; } } /// /// LB11: Do not break before or after word joiner. /// private readonly BreakAction LB11() => this.next.Is(LineBreakClass.WordJoiner) || this.current.Is(LineBreakClass.WordJoiner) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB12: Do not break after a glue character. /// private readonly BreakAction LB12() => this.current.Is(LineBreakClass.Glue) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB12a: Do not break before a glue character except after spaces, break-after, hyphen, or Hebrew hyphen. /// private readonly BreakAction LB12a() { if (this.next.Is(LineBreakClass.Glue) && !this.current.Is(LineBreakClass.Space) && !this.current.Is(LineBreakClass.BreakAfter) && !this.current.Is(LineBreakClass.Hyphen) && !this.current.Is(LineBreakClass.UnambiguousHyphen)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB13: Do not break before closing punctuation, closing parenthesis, exclamation/interrogation, /// or inseparable symbols. /// private readonly BreakAction LB13() => this.next.Is(LineBreakClass.ClosePunctuation) || this.next.Is(LineBreakClass.CloseParenthesis) || this.next.Is(LineBreakClass.Exclamation) || this.next.Is(LineBreakClass.BreakSymbols) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB14: Do not break after an opening punctuation, even after intervening spaces. /// private BreakAction LB14() { if (this.current.Is(LineBreakClass.OpenPunctuation)) { if (this.next.Is(LineBreakClass.Space)) { this.spaces = true; } return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB15a: Do not break after an initial quotation mark following a start-like context, /// even after intervening spaces. /// private BreakAction LB15a() { if (IsSotBkCrLfNlOpQuGlSpZw(this.previous) && this.current.Is(LineBreakClass.Quotation) && this.current.Category == UnicodeCategory.InitialQuotePunctuation) { this.spaces = true; return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB15b: Do not break before a final quotation mark when it closes a quotation-like run. /// private readonly BreakAction LB15b() { if (this.next.Is(LineBreakClass.Quotation) && this.next.Category == UnicodeCategory.FinalQuotePunctuation) { if (!this.TryGetAfterNext(out LineBreakCodePoint after) || IsSpGlWjClQuCpExIsSyBkCrLfNlZw(after)) { return BreakAction.NoBreak; } } return BreakAction.Pass; } /// /// LB15c: Permit a break between a space and an inseparable separator before a number. /// private readonly BreakAction LB15c() { if (this.current.Is(LineBreakClass.Space) && this.next.Is(LineBreakClass.InfixNumeric) && this.TryGetAfterNext(out LineBreakCodePoint after) && after.Is(LineBreakClass.Numeric)) { return BreakAction.MayBreak; } return BreakAction.Pass; } /// /// LB15d: Do not break before inseparable separators in other contexts. /// private readonly BreakAction LB15d() => this.next.Is(LineBreakClass.InfixNumeric) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB16: Do not break between closing punctuation or closing parenthesis and a nonstarter, /// even with intervening spaces. /// private BreakAction LB16() { if (this.current.Is(LineBreakClass.ClosePunctuation) || this.current.Is(LineBreakClass.CloseParenthesis)) { if (this.ClassAfterSpacesIs(this.current.CharEnd, LineBreakClass.Nonstarter)) { if (this.next.Is(LineBreakClass.Space)) { this.spaces = true; } return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.Space)) { return BreakAction.NoBreak; } } return BreakAction.Pass; } /// /// LB17: Do not break within balanced punctuation pairs, even with intervening spaces. /// private BreakAction LB17() { if (this.current.Is(LineBreakClass.BreakBeforeAndAfter)) { if (this.ClassAfterSpacesIs(this.current.CharEnd, LineBreakClass.BreakBeforeAndAfter)) { if (!this.next.Is(LineBreakClass.Space)) { return BreakAction.NoBreak; } this.spaces = true; return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.Space)) { return BreakAction.NoBreak; } } return BreakAction.Pass; } /// /// LB18: Break after spaces. /// private readonly BreakAction LB18() => this.current.Is(LineBreakClass.Space) ? BreakAction.MayBreak : BreakAction.Pass; /// /// LB19: Do not break before or after quotation marks. /// /// /// Initial and final quotation categories are handled by LB15a and LB15b where the standard /// gives them more specific behavior. /// private readonly BreakAction LB19() { if (this.next.Is(LineBreakClass.Quotation) && this.next.Category != UnicodeCategory.InitialQuotePunctuation) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.Quotation) && this.current.Category != UnicodeCategory.FinalQuotePunctuation) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB19a: Applies the East Asian quotation mark tailoring used by the Unicode line break tests. /// private readonly BreakAction LB19a() { if (!IsEastAsian(this.current) && this.next.Is(LineBreakClass.Quotation)) { return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.Quotation) && (!this.TryGetAfterNext(out LineBreakCodePoint after) || !IsEastAsian(after))) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.Quotation) && !IsEastAsian(this.next)) { return BreakAction.NoBreak; } if ((this.previous.IsStartOfText || !IsEastAsian(this.previous)) && this.current.Is(LineBreakClass.Quotation)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB20: Break before and after contingent break characters. /// private readonly BreakAction LB20() => this.current.Is(LineBreakClass.ContingentBreak) || this.next.Is(LineBreakClass.ContingentBreak) ? BreakAction.MayBreak : BreakAction.Pass; /// /// LB20a: Do not break after a leading hyphen or Hebrew hyphen before alphabetic text. /// private readonly BreakAction LB20a() { if (IsSotBkCrLfNlSpZwCbGl(this.previous) && (this.current.Is(LineBreakClass.Hyphen) || this.current.Is(LineBreakClass.UnambiguousHyphen)) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB21: Do not break before break-after, hyphen, Hebrew hyphen, or nonstarter; /// do not break after break-before. /// private readonly BreakAction LB21() { if (this.current.Is(LineBreakClass.BreakBefore) || this.next.Is(LineBreakClass.BreakAfter) || this.next.Is(LineBreakClass.UnambiguousHyphen) || this.next.Is(LineBreakClass.Hyphen) || this.next.Is(LineBreakClass.Nonstarter)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB21a: Do not break after Hebrew letters followed by hyphen or Hebrew hyphen. /// private readonly BreakAction LB21a() { if (this.previous.Is(LineBreakClass.HebrewLetter) && (this.current.Is(LineBreakClass.Hyphen) || this.current.Is(LineBreakClass.UnambiguousHyphen)) && !this.next.Is(LineBreakClass.HebrewLetter)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB21b: Do not break between solidus-like symbols and Hebrew letters. /// private readonly BreakAction LB21b() => this.current.Is(LineBreakClass.BreakSymbols) && this.next.Is(LineBreakClass.HebrewLetter) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB22: Do not break before ellipses and other inseparable characters. /// private readonly BreakAction LB22() => this.next.Is(LineBreakClass.Inseparable) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB23: Do not break between letters and numbers. /// private readonly BreakAction LB23() { if ((this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) && this.next.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.Numeric) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB23a: Do not break between numeric prefixes or postfixes and ideographs, emoji bases, /// or emoji modifiers. /// private readonly BreakAction LB23a() { if (this.current.Is(LineBreakClass.PrefixNumeric) && IsIdEbEm(this.next)) { return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.PostfixNumeric) && IsIdEbEm(this.current)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB24: Do not break between numeric prefixes or postfixes and alphabetic letters. /// private readonly BreakAction LB24() { if ((this.current.Is(LineBreakClass.PrefixNumeric) || this.current.Is(LineBreakClass.PostfixNumeric)) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter))) { return BreakAction.NoBreak; } if ((this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) && (this.next.Is(LineBreakClass.PrefixNumeric) || this.next.Is(LineBreakClass.PostfixNumeric))) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB25: Do not break within numeric expressions. /// /// /// This implementation covers the multi-code-point contexts from the rule by looking backward /// across SY/IS separators and forward across optional opening punctuation. The layout URL /// tailoring can later reintroduce a narrow solidus break inside recognized URL path segments. /// private readonly BreakAction LB25() { bool hasNumericScanEnd = false; int numericScanCharEnd = 0; if (this.next.Is(LineBreakClass.PostfixNumeric) || this.next.Is(LineBreakClass.PrefixNumeric)) { numericScanCharEnd = this.current.Is(LineBreakClass.ClosePunctuation) || this.current.Is(LineBreakClass.CloseParenthesis) ? this.previous.CharEnd : this.current.CharEnd; hasNumericScanEnd = true; } else if (this.next.Is(LineBreakClass.Numeric)) { numericScanCharEnd = this.current.CharEnd; hasNumericScanEnd = true; } if (hasNumericScanEnd) { int scanCharEnd = numericScanCharEnd; while (this.TryReadBackward(scanCharEnd, out LineBreakCodePoint codePoint)) { if (codePoint.Is(LineBreakClass.BreakSymbols) || codePoint.Is(LineBreakClass.InfixNumeric)) { scanCharEnd = codePoint.CharStart; continue; } if (codePoint.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } break; } } if (this.current.Is(LineBreakClass.PostfixNumeric) || this.current.Is(LineBreakClass.PrefixNumeric)) { if (this.next.Is(LineBreakClass.OpenPunctuation)) { if (this.TryGetAfterNext(out LineBreakCodePoint after)) { if (after.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } if (after.Is(LineBreakClass.InfixNumeric) && this.TryGetAfterNext(out LineBreakCodePoint afterAfter, 2) && afterAfter.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } } } else if (this.next.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } } if (this.current.Is(LineBreakClass.Hyphen) && this.next.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.InfixNumeric) && this.next.Is(LineBreakClass.Numeric)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB26: Do not break a Korean syllable block. /// private readonly BreakAction LB26() { if (this.current.Is(LineBreakClass.HangulLeadJamo) && IsJlJvH2H3(this.next)) { return BreakAction.NoBreak; } if ((this.current.Is(LineBreakClass.HangulVowelJamo) || this.current.Is(LineBreakClass.HangulLeadVowelSyllable)) && (this.next.Is(LineBreakClass.HangulVowelJamo) || this.next.Is(LineBreakClass.HangulTailJamo))) { return BreakAction.NoBreak; } if ((this.current.Is(LineBreakClass.HangulTailJamo) || this.current.Is(LineBreakClass.HangulLeadVowelTailSyllable)) && this.next.Is(LineBreakClass.HangulTailJamo)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB27: Treat Korean syllable blocks like ideographs for numeric prefix and postfix handling. /// private readonly BreakAction LB27() { if (IsJlJvJtH2H3(this.current) && this.next.Is(LineBreakClass.PostfixNumeric)) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.PrefixNumeric) && IsJlJvJtH2H3(this.next)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB28: Do not break between alphabetic letters. /// private readonly BreakAction LB28() => (this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter)) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter)) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB28a: Do not break inside orthographic syllables for Brahmic scripts. /// /// /// This is the Unicode 15+ aksara rule family. It keeps aksara bases, viramas, invisible /// stackers, and following bases together, with U+25CC DOTTED CIRCLE treated as a base. /// private readonly BreakAction LB28a() { if (this.current.Is(LineBreakClass.AksaraPrebase) && IsAksaraBase(this.next)) { return BreakAction.NoBreak; } if (IsAksaraBase(this.current) && (this.next.Is(LineBreakClass.ViramaFinal) || this.next.Is(LineBreakClass.Virama))) { return BreakAction.NoBreak; } if (IsAksaraBase(this.previous) && this.current.Is(LineBreakClass.Virama) && (this.next.Is(LineBreakClass.Aksara) || this.next.CodePoint.Value == DottedCircle)) { return BreakAction.NoBreak; } if (IsAksaraBase(this.current) && IsAksaraBase(this.next) && this.TryGetAfterNext(out LineBreakCodePoint after) && after.Is(LineBreakClass.ViramaFinal)) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB29: Do not break between numeric punctuation and alphabetic letters. /// private readonly BreakAction LB29() => this.current.Is(LineBreakClass.InfixNumeric) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter)) ? BreakAction.NoBreak : BreakAction.Pass; /// /// LB30: Do not break between letters or numbers and non-East-Asian opening or closing punctuation. /// private readonly BreakAction LB30() { if ((this.current.Is(LineBreakClass.Alphabetic) || this.current.Is(LineBreakClass.HebrewLetter) || this.current.Is(LineBreakClass.Numeric)) && this.next.Is(LineBreakClass.OpenPunctuation) && !IsEastAsian(this.next)) { return BreakAction.NoBreak; } if (this.current.Is(LineBreakClass.CloseParenthesis) && !IsEastAsian(this.current) && (this.next.Is(LineBreakClass.Alphabetic) || this.next.Is(LineBreakClass.HebrewLetter) || this.next.Is(LineBreakClass.Numeric))) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// LB30a: Break between regional indicator symbols only at even boundaries. /// /// /// This keeps flag emoji pairs together by forbidding the first RI/RI boundary in each run and /// allowing the next one. /// private BreakAction LB30a() { if (this.current.Is(LineBreakClass.RegionalIndicator) && this.next.Is(LineBreakClass.RegionalIndicator)) { this.regionalIndicatorCount++; if (this.regionalIndicatorCount % 2 != 0) { return BreakAction.NoBreak; } } return BreakAction.Pass; } /// /// LB30b: Do not break between emoji base characters and emoji modifiers. /// private readonly BreakAction LB30b() { if (this.current.Is(LineBreakClass.EmojiBase) && this.next.Is(LineBreakClass.EmojiModifier)) { return BreakAction.NoBreak; } if (this.next.Is(LineBreakClass.EmojiModifier) && this.current.Category == UnicodeCategory.OtherNotAssigned && CodePoint.GetGraphemeClusterClass(this.current.CodePoint) == GraphemeClusterClass.ExtendedPictographic) { return BreakAction.NoBreak; } return BreakAction.Pass; } /// /// Scans forward from over spaces and checks the first non-space class. /// /// /// LB16 and LB17 both have "with intervening spaces" forms. This helper performs that lookahead /// without advancing the streaming enumerator state. /// private readonly bool ClassAfterSpacesIs(int charIndex, LineBreakClass cls) { int scanChar = charIndex; int scanLength = this.current.Length; while (this.TryReadForward(scanChar, scanLength, out LineBreakCodePoint codePoint)) { if (!codePoint.Is(LineBreakClass.Space)) { return codePoint.Is(cls); } scanChar = codePoint.CharEnd; scanLength = codePoint.Length; } return false; } /// /// Reads a code point after without advancing the enumerator. /// /// The decoded lookahead code point. /// The number of code points after to inspect. /// when the requested lookahead exists. private readonly bool TryGetAfterNext(out LineBreakCodePoint codePoint, int offset = 1) { codePoint = default; int scanChar = this.next.CharEnd; int scanLength = this.next.Length; for (int i = 0; i < offset; i++) { if (!this.TryReadForward(scanChar, scanLength, out codePoint)) { return false; } scanChar = codePoint.CharEnd; scanLength = codePoint.Length; } return true; } /// /// Decodes a code point at without advancing the enumerator. /// /// The UTF-16 index to decode from. /// The code point length to assign to the decoded lookahead item. /// The decoded lookahead code point. /// when a code point was available. private readonly bool TryReadForward(int charIndex, int length, out LineBreakCodePoint codePoint) { if (!this.next.IsSentinel && charIndex == this.next.CharStart) { codePoint = this.next; return true; } if (charIndex >= this.source.Length) { codePoint = default; return false; } CodePoint cp = CodePoint.DecodeFromUtf16At(this.source, charIndex, out int charsConsumed); UnicodeCategory category = CodePoint.GetGeneralCategory(cp); LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(cp), category); codePoint = new LineBreakCodePoint( cp, cls, category, length + 1, charIndex, charIndex + charsConsumed); return true; } /// /// Decodes the code point ending at without moving the stream. /// /// /// Most callers hit the already-buffered , , or /// entries. Decoding from source is the fallback for longer LB25 or trimming /// scans and still does not allocate. /// /// The UTF-16 index immediately after the code point to read. /// The decoded lookbehind code point. /// when a code point was available. private readonly bool TryReadBackward(int charEnd, out LineBreakCodePoint codePoint) { if (!this.current.IsSentinel && charEnd == this.current.CharEnd) { codePoint = this.current; return true; } if (!this.previous.IsSentinel && charEnd == this.previous.CharEnd) { codePoint = this.previous; return true; } if (!this.next.IsSentinel && charEnd == this.next.CharEnd) { codePoint = this.next; return true; } if (charEnd <= 0) { codePoint = default; return false; } int charStart = charEnd - 1; if (charStart > 0 && char.IsLowSurrogate(this.source[charStart]) && char.IsHighSurrogate(this.source[charStart - 1])) { charStart--; } CodePoint cp = CodePoint.DecodeFromUtf16At(this.source, charStart, out int _); UnicodeCategory category = CodePoint.GetGeneralCategory(cp); LineBreakClass cls = MapClass(CodePoint.GetLineBreakClass(cp), category); codePoint = new LineBreakCodePoint(cp, cls, category, 0, charStart, charEnd); return true; } /// /// Walks backward from a wrap position to the nearest non-breaking trailing content so that /// measurement excludes trailing spaces and hard line terminators while wrapping still occurs /// at the original boundary. /// private readonly int FindPriorNonWhitespace(LineBreakCodePoint from) { int measure = from.Length; int charEnd = from.CharEnd; if (this.TryReadBackward(charEnd, out LineBreakCodePoint codePoint) && (codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.CarriageReturn))) { measure--; charEnd = codePoint.CharStart; } while (this.TryReadBackward(charEnd, out codePoint)) { if (codePoint.Is(LineBreakClass.Space)) { measure--; charEnd = codePoint.CharStart; } else { break; } } return measure; } /// /// Checks the class exclusions used by LB9 before combining marks are folded into their base. /// private static bool IsBkCrLfNlSpZw(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.CarriageReturn) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.NextLine) || codePoint.Is(LineBreakClass.Space) || codePoint.Is(LineBreakClass.ZeroWidthSpace); /// /// Checks the start-like contexts that allow LB15a initial quotation handling. /// private static bool IsSotBkCrLfNlOpQuGlSpZw(LineBreakCodePoint codePoint) => codePoint.IsStartOfText || codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.CarriageReturn) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.NextLine) || codePoint.Is(LineBreakClass.OpenPunctuation) || codePoint.Is(LineBreakClass.Quotation) || codePoint.Is(LineBreakClass.Glue) || codePoint.Is(LineBreakClass.Space) || codePoint.Is(LineBreakClass.ZeroWidthSpace); /// /// Checks the classes that may follow a final quotation mark for LB15b. /// private static bool IsSpGlWjClQuCpExIsSyBkCrLfNlZw(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.Space) || codePoint.Is(LineBreakClass.Glue) || codePoint.Is(LineBreakClass.WordJoiner) || codePoint.Is(LineBreakClass.ClosePunctuation) || codePoint.Is(LineBreakClass.Quotation) || codePoint.Is(LineBreakClass.CloseParenthesis) || codePoint.Is(LineBreakClass.Exclamation) || codePoint.Is(LineBreakClass.InfixNumeric) || codePoint.Is(LineBreakClass.BreakSymbols) || codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.CarriageReturn) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.NextLine) || codePoint.Is(LineBreakClass.ZeroWidthSpace); /// /// Checks the leading contexts used by LB20a for hyphenated words. /// private static bool IsSotBkCrLfNlSpZwCbGl(LineBreakCodePoint codePoint) => codePoint.IsStartOfText || codePoint.Is(LineBreakClass.MandatoryBreak) || codePoint.Is(LineBreakClass.CarriageReturn) || codePoint.Is(LineBreakClass.LineFeed) || codePoint.Is(LineBreakClass.NextLine) || codePoint.Is(LineBreakClass.Space) || codePoint.Is(LineBreakClass.ZeroWidthSpace) || codePoint.Is(LineBreakClass.ContingentBreak) || codePoint.Is(LineBreakClass.Glue); /// /// Checks the ideographic and emoji classes that participate in LB23a and LB27. /// private static bool IsIdEbEm(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.Ideographic) || codePoint.Is(LineBreakClass.EmojiBase) || codePoint.Is(LineBreakClass.EmojiModifier); /// /// Checks the Hangul classes allowed after a leading jamo for LB26. /// private static bool IsJlJvH2H3(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.HangulLeadJamo) || codePoint.Is(LineBreakClass.HangulVowelJamo) || codePoint.Is(LineBreakClass.HangulLeadVowelSyllable) || codePoint.Is(LineBreakClass.HangulLeadVowelTailSyllable); /// /// Checks the Hangul syllable-block classes used by LB27. /// private static bool IsJlJvJtH2H3(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.HangulLeadJamo) || codePoint.Is(LineBreakClass.HangulVowelJamo) || codePoint.Is(LineBreakClass.HangulTailJamo) || codePoint.Is(LineBreakClass.HangulLeadVowelSyllable) || codePoint.Is(LineBreakClass.HangulLeadVowelTailSyllable); /// /// Checks whether a code point is an aksara base for LB28a. /// private static bool IsAksaraBase(LineBreakCodePoint codePoint) => codePoint.Is(LineBreakClass.Aksara) || codePoint.Is(LineBreakClass.AksaraStart) || codePoint.CodePoint.Value == DottedCircle; /// /// Checks whether a code point has East Asian width for LB19a and LB30 punctuation behavior. /// private static bool IsEastAsian(LineBreakCodePoint codePoint) { if (codePoint.IsSentinel) { return false; } EastAsianWidthClass width = CodePoint.GetEastAsianWidthClass(codePoint.CodePoint); return width is EastAsianWidthClass.Fullwidth or EastAsianWidthClass.Halfwidth or EastAsianWidthClass.Wide; } /// /// Determines where a plain-text run should stop while looking for URL markers. /// private static bool IsUrlRunBoundary(CodePoint codePoint) { if (CodePoint.IsWhiteSpace(codePoint)) { return true; } return codePoint.Value is QuotationMark or Apostrophe or LessThanSign or GreaterThanSign; } /// /// Determines whether is valid after the first URI scheme character. /// private static bool IsUrlSchemeCharacter(CodePoint codePoint) => (codePoint.IsAscii && CodePoint.IsLetterOrDigit(codePoint)) || codePoint.Value is PlusSign or HyphenMinus or FullStop; /// /// Determines whether is valid as the first URI scheme character. /// private static bool IsUrlSchemeStartCharacter(CodePoint codePoint) => codePoint.IsAscii && CodePoint.IsLetter(codePoint); /// /// Determines whether may be part of the host prefix check. /// private static bool IsUrlHostCharacter(CodePoint codePoint) => (codePoint.IsAscii && CodePoint.IsLetterOrDigit(codePoint)) || codePoint.Value is HyphenMinus or FullStop; /// /// Determines whether is ASCII W or w. /// private static bool IsAsciiW(CodePoint codePoint) => codePoint.Value is UppercaseW or LowercaseW; /// /// Streaming recognizer for the URL-shaped tokens needed by UAX #14 section 8 tailoring. /// /// /// This is deliberately not a URI parser. It recognizes two common plain-text signals while the /// main line-break stream is already decoding the source: a valid ASCII URI scheme followed by /// ://, or a www. prefix at a host-label boundary. Once a run is URL-like, later /// solidus boundaries in that run can use the tailored behavior without rescanning the text. /// private struct UrlTailoringState { /// /// Length of the current ASCII URI-scheme candidate, or zero when no scheme is active. /// private int schemeLength; /// /// Number of consecutive ASCII w or W characters in a possible www. prefix. /// private int wwwPrefixLength; /// /// Indicates that the current non-boundary run has already matched a URL signal. /// private bool isUrlLikeRun; /// /// Indicates that the previous code point was : ending a valid scheme candidate. /// private bool previousWasColonAfterValidScheme; /// /// Indicates that the previous code point was the first slash in a :// marker. /// private bool previousWasFirstSchemeSlash; /// /// Indicates that the previous code point could be part of an ASCII host label. /// private bool previousWasHostCharacter; /// /// Blocks scheme recognition until a non-scheme character resets the candidate. /// /// /// URI schemes must start with an ASCII letter. A run such as 1http: should not /// become valid just because later characters are allowed inside a scheme. /// private bool schemeBlocked; /// /// Updates the recognizer with the next decoded code point. /// /// The code point from the main line-break stream. /// when this code point belongs to a URL-like run. public bool Update(CodePoint codePoint) { if (IsUrlRunBoundary(codePoint)) { this = default; return false; } bool currentIsUrlLike = this.isUrlLikeRun; bool currentWasColonAfterValidScheme = false; bool currentWasFirstSchemeSlash = false; if (codePoint.Value == Solidus) { if (this.previousWasFirstSchemeSlash) { this.isUrlLikeRun = true; currentIsUrlLike = true; } currentWasFirstSchemeSlash = this.previousWasColonAfterValidScheme; this.schemeLength = 0; this.schemeBlocked = false; this.wwwPrefixLength = 0; } else { this.UpdateSchemeState(codePoint, out currentWasColonAfterValidScheme); this.UpdateWwwPrefixState(codePoint, ref currentIsUrlLike); } this.previousWasColonAfterValidScheme = currentWasColonAfterValidScheme; this.previousWasFirstSchemeSlash = currentWasFirstSchemeSlash; this.previousWasHostCharacter = IsUrlHostCharacter(codePoint); return currentIsUrlLike; } /// /// Updates the ASCII URI-scheme candidate state. /// /// The code point from the main line-break stream. /// /// Set to when is the colon after /// a valid URI scheme candidate. /// private void UpdateSchemeState(CodePoint codePoint, out bool currentWasColonAfterValidScheme) { currentWasColonAfterValidScheme = false; if (codePoint.Value == Colon) { currentWasColonAfterValidScheme = this.schemeLength > 0; this.schemeLength = 0; this.schemeBlocked = false; return; } if (IsUrlSchemeCharacter(codePoint)) { if (this.schemeLength > 0) { this.schemeLength++; } else if (!this.schemeBlocked && IsUrlSchemeStartCharacter(codePoint)) { this.schemeLength = 1; } else { this.schemeBlocked = true; } return; } this.schemeLength = 0; this.schemeBlocked = false; } /// /// Updates the www. prefix recognizer. /// /// The code point from the main line-break stream. /// /// The URL-like status to return for the current code point, updated when the prefix completes. /// private void UpdateWwwPrefixState(CodePoint codePoint, ref bool currentIsUrlLike) { if (this.isUrlLikeRun) { currentIsUrlLike = true; return; } if (this.wwwPrefixLength == 3 && codePoint.Value == FullStop) { this.isUrlLikeRun = true; currentIsUrlLike = true; this.wwwPrefixLength = 0; return; } if (!IsAsciiW(codePoint)) { this.wwwPrefixLength = 0; return; } if (!this.previousWasHostCharacter) { this.wwwPrefixLength = 1; } else if (this.wwwPrefixLength is 1 or 2) { this.wwwPrefixLength++; } else { this.wwwPrefixLength = 0; } } } /// /// The decoded code point plus the UAX #14 state needed to evaluate a boundary. /// /// /// The struct stores both code point and UTF-16 positions so the enumerator can stream over the /// original span, trim trailing whitespace for measurement, and perform bounded lookahead/lookbehind /// without allocating intermediate collections. /// private struct LineBreakCodePoint { /// /// Initializes a new instance of the struct for a real code point. /// /// The decoded Unicode scalar or replacement character. /// The LB1-resolved line break class. /// The general category for quote and emoji-specific rules. /// The one-based code point index immediately after this item. /// The UTF-16 index where this code point starts. /// The UTF-16 index immediately after this code point. /// Whether this item belongs to a URL-like run for layout tailoring. public LineBreakCodePoint( CodePoint codePoint, LineBreakClass cls, UnicodeCategory category, int length, int charStart, int charEnd, bool isUrlLikeRun = false) { this.CodePoint = codePoint; this.Class = cls; this.Category = category; this.Length = length; this.CharStart = charStart; this.CharEnd = charEnd; this.SentinelValue = 0; this.Ignored = false; this.IsUrlLikeRun = isUrlLikeRun; } /// /// Initializes a new instance of the struct as a sentinel /// representing start or end of text. /// /// The sentinel value. /// The code point length associated with the sentinel boundary. /// The UTF-16 boundary associated with the sentinel. private LineBreakCodePoint(int sentinel, int length, int charEnd) { this.CodePoint = default; this.Class = default; this.Category = default; this.Length = length; this.CharStart = charEnd; this.CharEnd = charEnd; this.SentinelValue = sentinel; this.Ignored = false; this.IsUrlLikeRun = false; } /// /// Gets the decoded code point. /// public CodePoint CodePoint { get; } /// /// Gets or sets the LB1-resolved line break class. /// /// /// LB10 can rewrite a remaining CM or ZWJ to AL after LB9 has handled attached marks. /// public LineBreakClass Class { get; set; } /// /// Gets the Unicode general category for quote and emoji-context checks. /// public UnicodeCategory Category { get; } /// /// Gets or sets the one-based code point index immediately after this item. /// /// /// LB9 ignored marks extend the current item, so updates this value when /// a mark is folded into its base. /// public int Length { get; set; } /// /// Gets the UTF-16 index where this item starts. /// public int CharStart { get; } /// /// Gets or sets the UTF-16 index immediately after this item. /// /// /// LB9 ignored marks extend the current item, so updates this value when /// a mark is folded into its base. /// public int CharEnd { get; set; } /// /// Gets the sentinel value, or zero for a real code point. /// public int SentinelValue { get; } /// /// Gets or sets a value indicating whether LB9 folded this item into the previous base. /// public bool Ignored { get; set; } /// /// Gets a value indicating whether this item belongs to a URL-like run for layout tailoring. /// public bool IsUrlLikeRun { get; } /// /// Gets a value indicating whether this item is a start or end sentinel. /// public readonly bool IsSentinel => this.SentinelValue != 0; /// /// Gets a value indicating whether this item is the start-of-text sentinel. /// public readonly bool IsStartOfText => this.SentinelValue == StartOfText; /// /// Gets a value indicating whether this item is the end-of-text sentinel. /// public readonly bool IsEndOfText => this.SentinelValue == EndOfText; /// /// Creates a start-of-text or end-of-text sentinel. /// /// The sentinel value to assign. /// The code point boundary associated with the sentinel. /// The UTF-16 boundary associated with the sentinel. /// The sentinel item. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static LineBreakCodePoint CreateSentinel(int sentinel, int length, int charEnd) => new(sentinel, length, charEnd); /// /// Checks whether this item has the given line break class. /// /// The class to compare. /// when this is a real item with the requested class. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Is(LineBreakClass cls) => !this.IsSentinel && this.Class == cls; /// /// Checks whether this item has the given scalar value. /// /// The scalar value to compare. /// when this is a real item with the requested value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool HasValue(int value) => !this.IsSentinel && this.CodePoint.Value == value; } } }