part of dart._engine; // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. const Set _kNewlines = { 0x000A, // LF 0x000B, // BK 0x000C, // BK 0x000D, // CR 0x0085, // NL 0x2028, // BK 0x2029, // BK }; const Set _kSpaces = { 0x0020, // SP 0x200B, // ZW }; /// Various types of line breaks as defined by the Unicode spec. enum LineBreakType { /// Indicates that a line break is possible but not mandatory. opportunity, /// Indicates that a line break isn't possible. prohibited, /// Indicates that this is a hard line break that can't be skipped. mandatory, /// Indicates the end of the text (which is also considered a line break in /// the Unicode spec). This is the same as [mandatory] but it's needed in our /// implementation to distinguish between the universal [endOfText] and the /// line break caused by "\n" at the end of the text. endOfText, } /// Splits [text] into fragments based on line breaks. abstract class LineBreakFragmenter extends TextFragmenter { factory LineBreakFragmenter(String text) { if (domIntl.v8BreakIterator != null) { return V8LineBreakFragmenter(text); } return FWLineBreakFragmenter(text); } @override List fragment(); } /// Flutter web's custom implementation of [LineBreakFragmenter]. class FWLineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter { FWLineBreakFragmenter(super.text); @override List fragment() { return _computeLineBreakFragments(text); } } /// An implementation of [LineBreakFragmenter] that uses V8's /// `v8BreakIterator` API to find line breaks in the given [text]. class V8LineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter { V8LineBreakFragmenter(super.text) : assert(domIntl.v8BreakIterator != null); final DomV8BreakIterator _v8BreakIterator = createV8BreakIterator(); @override List fragment() { return breakLinesUsingV8BreakIterator(text, text.toJS, _v8BreakIterator); } } List breakLinesUsingV8BreakIterator( String text, JSString jsText, DomV8BreakIterator iterator, ) { final List breaks = []; int fragmentStart = 0; iterator.adoptText(jsText); iterator.first(); while (iterator.next() != -1) { final int fragmentEnd = iterator.current().toInt(); int trailingNewlines = 0; int trailingSpaces = 0; // Calculate trailing newlines and spaces. for (int i = fragmentStart; i < fragmentEnd; i++) { final int codeUnit = text.codeUnitAt(i); if (_kNewlines.contains(codeUnit)) { trailingNewlines++; trailingSpaces++; } else if (_kSpaces.contains(codeUnit)) { trailingSpaces++; } else { // Always break after a sequence of spaces. if (trailingSpaces > 0) { breaks.add( LineBreakFragment( fragmentStart, i, LineBreakType.opportunity, trailingNewlines: trailingNewlines, trailingSpaces: trailingSpaces, ), ); fragmentStart = i; trailingNewlines = 0; trailingSpaces = 0; } } } final LineBreakType type; if (trailingNewlines > 0) { type = LineBreakType.mandatory; } else if (fragmentEnd == text.length) { type = LineBreakType.endOfText; } else { type = LineBreakType.opportunity; } breaks.add( LineBreakFragment( fragmentStart, fragmentEnd, type, trailingNewlines: trailingNewlines, trailingSpaces: trailingSpaces, ), ); fragmentStart = fragmentEnd; } if (breaks.isEmpty || breaks.last.type == LineBreakType.mandatory) { breaks.add( LineBreakFragment( text.length, text.length, LineBreakType.endOfText, trailingNewlines: 0, trailingSpaces: 0, ), ); } return breaks; } class LineBreakFragment extends TextFragment { const LineBreakFragment( super.start, super.end, this.type, { required this.trailingNewlines, required this.trailingSpaces, }); final LineBreakType type; final int trailingNewlines; final int trailingSpaces; @override int get hashCode => Object.hash(start, end, type, trailingNewlines, trailingSpaces); @override bool operator ==(Object other) { return other is LineBreakFragment && other.start == start && other.end == end && other.type == type && other.trailingNewlines == trailingNewlines && other.trailingSpaces == trailingSpaces; } @override String toString() { return 'LineBreakFragment($start, $end, $type)'; } } bool _isHardBreak(LineCharProperty? prop) { // No need to check for NL because it's already normalized to BK. return prop == LineCharProperty.LF || prop == LineCharProperty.BK; } bool _isALorHL(LineCharProperty? prop) { return prop == LineCharProperty.AL || prop == LineCharProperty.HL; } /// Whether the given property is part of a Korean Syllable block. /// /// See: /// - https://unicode.org/reports/tr14/tr14-45.html#LB27 bool _isKoreanSyllable(LineCharProperty? prop) { return prop == LineCharProperty.JL || prop == LineCharProperty.JV || prop == LineCharProperty.JT || prop == LineCharProperty.H2 || prop == LineCharProperty.H3; } /// Whether the given char code has an Eastern Asian width property of F, W or H. /// /// See: /// - https://www.unicode.org/reports/tr14/tr14-45.html#LB30 /// - https://www.unicode.org/Public/13.0.0/ucd/EastAsianWidth.txt bool _hasEastAsianWidthFWH(int charCode) { return charCode == 0x2329 || (charCode >= 0x3008 && charCode <= 0x301D) || (charCode >= 0xFE17 && charCode <= 0xFF62); } bool _isSurrogatePair(int? codePoint) { return codePoint != null && codePoint > 0xFFFF; } /// Finds the next line break in the given [text] starting from [index]. /// /// We think about indices as pointing between characters, and they go all the /// way from 0 to the string length. For example, here are the indices for the /// string "foo bar": /// /// ```none /// f o o b a r /// ^ ^ ^ ^ ^ ^ ^ ^ /// 0 1 2 3 4 5 6 7 /// ``` /// /// This way the indices work well with [String.substring]. /// /// Useful resources: /// /// * https://www.unicode.org/reports/tr14/tr14-45.html#Algorithm /// * https://www.unicode.org/Public/11.0.0/ucd/LineBreak.txt List _computeLineBreakFragments(String text) { final List fragments = []; // Keeps track of the character two positions behind. LineCharProperty? prev2; LineCharProperty? prev1; int? codePoint = getCodePoint(text, 0); LineCharProperty? curr = lineLookup.findForChar(codePoint); // When there's a sequence of combining marks, this variable contains the base // property i.e. the property of the character preceding the sequence. LineCharProperty baseOfCombiningMarks = LineCharProperty.AL; int index = 0; int trailingNewlines = 0; int trailingSpaces = 0; int fragmentStart = 0; void setBreak(LineBreakType type, int debugRuleNumber) { final int fragmentEnd = type == LineBreakType.endOfText ? text.length : index; assert(fragmentEnd >= fragmentStart); // Uncomment the following line to help debug line breaking. // print('{$fragmentStart:$fragmentEnd} [$debugRuleNumber] -- $type'); if (prev1 == LineCharProperty.SP) { trailingSpaces++; } else if (_isHardBreak(prev1) || prev1 == LineCharProperty.CR) { trailingNewlines++; trailingSpaces++; } if (type == LineBreakType.prohibited) { // Don't create a fragment. return; } fragments.add( LineBreakFragment( fragmentStart, fragmentEnd, type, trailingNewlines: trailingNewlines, trailingSpaces: trailingSpaces, ), ); fragmentStart = index; // Reset trailing spaces/newlines counter after a new fragment. trailingNewlines = 0; trailingSpaces = 0; prev1 = prev2 = null; } // Never break at the start of text. // LB2: sot × setBreak(LineBreakType.prohibited, 2); // Never break at the start of text. // LB2: sot × // // Skip index 0 because a line break can't exist at the start of text. index++; int regionalIndicatorCount = 0; // We need to go until `text.length` in order to handle the case where the // paragraph ends with a hard break. In this case, there will be an empty line // at the end. for (; index <= text.length; index++) { prev2 = prev1; prev1 = curr; if (_isSurrogatePair(codePoint)) { // Can't break in the middle of a surrogate pair. setBreak(LineBreakType.prohibited, -1); // Advance `index` one extra step to skip the tail of the surrogate pair. index++; } codePoint = getCodePoint(text, index); curr = lineLookup.findForChar(codePoint); // Keep count of the RI (regional indicator) sequence. if (prev1 == LineCharProperty.RI) { regionalIndicatorCount++; } else { regionalIndicatorCount = 0; } // Always break after hard line breaks. // LB4: BK ! // // Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks. // LB5: LF ! // NL ! if (_isHardBreak(prev1)) { setBreak(LineBreakType.mandatory, 5); continue; } if (prev1 == LineCharProperty.CR) { if (curr == LineCharProperty.LF) { // LB5: CR × LF setBreak(LineBreakType.prohibited, 5); } else { // LB5: CR ! setBreak(LineBreakType.mandatory, 5); } continue; } // Do not break before hard line breaks. // LB6: × ( BK | CR | LF | NL ) if (_isHardBreak(curr) || curr == LineCharProperty.CR) { setBreak(LineBreakType.prohibited, 6); continue; } if (index >= text.length) { break; } // Do not break before spaces or zero width space. // LB7: × SP // × ZW if (curr == LineCharProperty.SP || curr == LineCharProperty.ZW) { setBreak(LineBreakType.prohibited, 7); continue; } // Break after spaces. // LB18: SP ÷ if (prev1 == LineCharProperty.SP) { setBreak(LineBreakType.opportunity, 18); continue; } // Break before any character following a zero-width space, even if one or // more spaces intervene. // LB8: ZW SP* ÷ if (prev1 == LineCharProperty.ZW) { setBreak(LineBreakType.opportunity, 8); continue; } // Do not break after a zero width joiner. // LB8a: ZWJ × if (prev1 == LineCharProperty.ZWJ) { setBreak(LineBreakType.prohibited, 8); continue; } // Establish the base for the sequences of combining marks. if (prev1 != LineCharProperty.CM && prev1 != LineCharProperty.ZWJ) { baseOfCombiningMarks = prev1 ?? LineCharProperty.AL; } // Do not break a combining character sequence; treat it as if it has the // line breaking class of the base character in all of the following rules. // Treat ZWJ as if it were CM. if (curr == LineCharProperty.CM || curr == LineCharProperty.ZWJ) { if (baseOfCombiningMarks == LineCharProperty.SP) { // LB10: Treat any remaining combining mark or ZWJ as AL. curr = LineCharProperty.AL; } else { // LB9: Treat X (CM | ZWJ)* as if it were X // where X is any line break class except BK, NL, LF, CR, SP, or ZW. curr = baseOfCombiningMarks; if (curr == LineCharProperty.RI) { // Prevent the previous RI from being double-counted. regionalIndicatorCount--; } setBreak(LineBreakType.prohibited, 9); continue; } } // In certain situations (e.g. CM immediately following a hard break), we // need to also check if the previous character was CM/ZWJ. That's because // hard breaks caused the previous iteration to short-circuit, which leads // to `baseOfCombiningMarks` not being updated properly. if (prev1 == LineCharProperty.CM || prev1 == LineCharProperty.ZWJ) { prev1 = baseOfCombiningMarks; } // Do not break before or after Word joiner and related characters. // LB11: × WJ // WJ × if (curr == LineCharProperty.WJ || prev1 == LineCharProperty.WJ) { setBreak(LineBreakType.prohibited, 11); continue; } // Do not break after NBSP and related characters. // LB12: GL × if (prev1 == LineCharProperty.GL) { setBreak(LineBreakType.prohibited, 12); continue; } // Do not break before NBSP and related characters, except after spaces and // hyphens. // LB12a: [^SP BA HY] × GL if (!(prev1 == LineCharProperty.SP || prev1 == LineCharProperty.BA || prev1 == LineCharProperty.HY) && curr == LineCharProperty.GL) { setBreak(LineBreakType.prohibited, 12); continue; } // Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces. // LB13: × CL // × CP // × EX // × IS // × SY // // The above is a quote from unicode.org. In our implementation, we did the // following modification: When there are spaces present, we consider it a // line break opportunity. // // We made this modification to match the browser behavior. if (prev1 != LineCharProperty.SP && (curr == LineCharProperty.CL || curr == LineCharProperty.CP || curr == LineCharProperty.EX || curr == LineCharProperty.IS || curr == LineCharProperty.SY)) { setBreak(LineBreakType.prohibited, 13); continue; } // Do not break after ‘[’, even after spaces. // LB14: OP SP* × // // The above is a quote from unicode.org. In our implementation, we did the // following modification: Allow breaks when there are spaces. // // We made this modification to match the browser behavior. if (prev1 == LineCharProperty.OP) { setBreak(LineBreakType.prohibited, 14); continue; } // Do not break within ‘”[’, even with intervening spaces. // LB15: QU SP* × OP // // The above is a quote from unicode.org. In our implementation, we did the // following modification: Allow breaks when there are spaces. // // We made this modification to match the browser behavior. if (prev1 == LineCharProperty.QU && curr == LineCharProperty.OP) { setBreak(LineBreakType.prohibited, 15); continue; } // Do not break between closing punctuation and a nonstarter, even with // intervening spaces. // LB16: (CL | CP) SP* × NS // // The above is a quote from unicode.org. In our implementation, we did the // following modification: Allow breaks when there are spaces. // // We made this modification to match the browser behavior. if ((prev1 == LineCharProperty.CL || prev1 == LineCharProperty.CP) && curr == LineCharProperty.NS) { setBreak(LineBreakType.prohibited, 16); continue; } // Do not break within ‘——’, even with intervening spaces. // LB17: B2 SP* × B2 // // The above is a quote from unicode.org. In our implementation, we did the // following modification: Allow breaks when there are spaces. // // We made this modification to match the browser behavior. if (prev1 == LineCharProperty.B2 && curr == LineCharProperty.B2) { setBreak(LineBreakType.prohibited, 17); continue; } // Do not break before or after quotation marks, such as ‘”’. // LB19: × QU // QU × if (prev1 == LineCharProperty.QU || curr == LineCharProperty.QU) { setBreak(LineBreakType.prohibited, 19); continue; } // Break before and after unresolved CB. // LB20: ÷ CB // CB ÷ // // In flutter web, we use this as an object-replacement character for // placeholders. if (prev1 == LineCharProperty.CB || curr == LineCharProperty.CB) { setBreak(LineBreakType.opportunity, 20); continue; } // Do not break before hyphen-minus, other hyphens, fixed-width spaces, // small kana, and other non-starters, or after acute accents. // LB21: × BA // × HY // × NS // BB × if (curr == LineCharProperty.BA || curr == LineCharProperty.HY || curr == LineCharProperty.NS || prev1 == LineCharProperty.BB) { setBreak(LineBreakType.prohibited, 21); continue; } // Don't break after Hebrew + Hyphen. // LB21a: HL (HY | BA) × if (prev2 == LineCharProperty.HL && (prev1 == LineCharProperty.HY || prev1 == LineCharProperty.BA)) { setBreak(LineBreakType.prohibited, 21); continue; } // Don’t break between Solidus and Hebrew letters. // LB21b: SY × HL if (prev1 == LineCharProperty.SY && curr == LineCharProperty.HL) { setBreak(LineBreakType.prohibited, 21); continue; } // Do not break before ellipses. // LB22: × IN if (curr == LineCharProperty.IN) { setBreak(LineBreakType.prohibited, 22); continue; } // Do not break between digits and letters. // LB23: (AL | HL) × NU // NU × (AL | HL) if ((_isALorHL(prev1) && curr == LineCharProperty.NU) || (prev1 == LineCharProperty.NU && _isALorHL(curr))) { setBreak(LineBreakType.prohibited, 23); continue; } // Do not break between numeric prefixes and ideographs, or between // ideographs and numeric postfixes. // LB23a: PR × (ID | EB | EM) if (prev1 == LineCharProperty.PR && (curr == LineCharProperty.ID || curr == LineCharProperty.EB || curr == LineCharProperty.EM)) { setBreak(LineBreakType.prohibited, 23); continue; } // LB23a: (ID | EB | EM) × PO if ((prev1 == LineCharProperty.ID || prev1 == LineCharProperty.EB || prev1 == LineCharProperty.EM) && curr == LineCharProperty.PO) { setBreak(LineBreakType.prohibited, 23); continue; } // Do not break between numeric prefix/postfix and letters, or between // letters and prefix/postfix. // LB24: (PR | PO) × (AL | HL) if ((prev1 == LineCharProperty.PR || prev1 == LineCharProperty.PO) && _isALorHL(curr)) { setBreak(LineBreakType.prohibited, 24); continue; } // LB24: (AL | HL) × (PR | PO) if (_isALorHL(prev1) && (curr == LineCharProperty.PR || curr == LineCharProperty.PO)) { setBreak(LineBreakType.prohibited, 24); continue; } // Do not break between the following pairs of classes relevant to numbers. // LB25: (CL | CP | NU) × (PO | PR) if ((prev1 == LineCharProperty.CL || prev1 == LineCharProperty.CP || prev1 == LineCharProperty.NU) && (curr == LineCharProperty.PO || curr == LineCharProperty.PR)) { setBreak(LineBreakType.prohibited, 25); continue; } // LB25: (PO | PR) × OP if ((prev1 == LineCharProperty.PO || prev1 == LineCharProperty.PR) && curr == LineCharProperty.OP) { setBreak(LineBreakType.prohibited, 25); continue; } // LB25: (PO | PR | HY | IS | NU | SY) × NU if ((prev1 == LineCharProperty.PO || prev1 == LineCharProperty.PR || prev1 == LineCharProperty.HY || prev1 == LineCharProperty.IS || prev1 == LineCharProperty.NU || prev1 == LineCharProperty.SY) && curr == LineCharProperty.NU) { setBreak(LineBreakType.prohibited, 25); continue; } // Do not break a Korean syllable. // LB26: JL × (JL | JV | H2 | H3) if (prev1 == LineCharProperty.JL && (curr == LineCharProperty.JL || curr == LineCharProperty.JV || curr == LineCharProperty.H2 || curr == LineCharProperty.H3)) { setBreak(LineBreakType.prohibited, 26); continue; } // LB26: (JV | H2) × (JV | JT) if ((prev1 == LineCharProperty.JV || prev1 == LineCharProperty.H2) && (curr == LineCharProperty.JV || curr == LineCharProperty.JT)) { setBreak(LineBreakType.prohibited, 26); continue; } // LB26: (JT | H3) × JT if ((prev1 == LineCharProperty.JT || prev1 == LineCharProperty.H3) && curr == LineCharProperty.JT) { setBreak(LineBreakType.prohibited, 26); continue; } // Treat a Korean Syllable Block the same as ID. // LB27: (JL | JV | JT | H2 | H3) × PO if (_isKoreanSyllable(prev1) && curr == LineCharProperty.PO) { setBreak(LineBreakType.prohibited, 27); continue; } // LB27: PR × (JL | JV | JT | H2 | H3) if (prev1 == LineCharProperty.PR && _isKoreanSyllable(curr)) { setBreak(LineBreakType.prohibited, 27); continue; } // Do not break between alphabetics. // LB28: (AL | HL) × (AL | HL) if (_isALorHL(prev1) && _isALorHL(curr)) { setBreak(LineBreakType.prohibited, 28); continue; } // Do not break between numeric punctuation and alphabetics (“e.g.”). // LB29: IS × (AL | HL) if (prev1 == LineCharProperty.IS && _isALorHL(curr)) { setBreak(LineBreakType.prohibited, 29); continue; } // Do not break between letters, numbers, or ordinary symbols and opening or // closing parentheses. // LB30: (AL | HL | NU) × OP // // LB30 requires that we exclude characters that have an Eastern Asian width // property of value F, W or H classes. if ((_isALorHL(prev1) || prev1 == LineCharProperty.NU) && curr == LineCharProperty.OP && !_hasEastAsianWidthFWH(text.codeUnitAt(index))) { setBreak(LineBreakType.prohibited, 30); continue; } // LB30: CP × (AL | HL | NU) if (prev1 == LineCharProperty.CP && !_hasEastAsianWidthFWH(text.codeUnitAt(index - 1)) && (_isALorHL(curr) || curr == LineCharProperty.NU)) { setBreak(LineBreakType.prohibited, 30); continue; } // Break between two regional indicator symbols if and only if there are an // even number of regional indicators preceding the position of the break. // LB30a: sot (RI RI)* RI × RI // [^RI] (RI RI)* RI × RI if (curr == LineCharProperty.RI) { if (regionalIndicatorCount.isOdd) { setBreak(LineBreakType.prohibited, 30); } else { setBreak(LineBreakType.opportunity, 30); } continue; } // Do not break between an emoji base and an emoji modifier. // LB30b: EB × EM if (prev1 == LineCharProperty.EB && curr == LineCharProperty.EM) { setBreak(LineBreakType.prohibited, 30); continue; } // Break everywhere else. // LB31: ALL ÷ // ÷ ALL setBreak(LineBreakType.opportunity, 31); } // Always break at the end of text. // LB3: ! eot setBreak(LineBreakType.endOfText, 3); return fragments; }