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. enum FragmentFlow { /// The fragment flows from left to right regardless of its surroundings. ltr, /// The fragment flows from right to left regardless of its surroundings. rtl, /// The fragment flows the same as the previous fragment. /// /// If it's the first fragment in a line, then it flows the same as the /// paragraph direction. /// /// E.g. digits. previous, /// If the previous and next fragments flow in the same direction, then this /// fragment flows in that same direction. Otherwise, it flows the same as the /// paragraph direction. /// /// E.g. spaces, symbols. sandwich, } /// Splits [text] into fragments based on directionality. class BidiFragmenter extends TextFragmenter { const BidiFragmenter(super.text); @override List fragment() { return _computeBidiFragments(text); } } class BidiFragment extends TextFragment { const BidiFragment(super.start, super.end, this.textDirection, this.fragmentFlow); final ui.TextDirection? textDirection; final FragmentFlow fragmentFlow; @override int get hashCode => Object.hash(start, end, textDirection, fragmentFlow); @override bool operator ==(Object other) { return other is BidiFragment && other.start == start && other.end == end && other.textDirection == textDirection && other.fragmentFlow == fragmentFlow; } @override String toString() { return 'BidiFragment($start, $end, $textDirection)'; } } // This data was taken from the source code of the Closure library: // // - https://github.com/google/closure-library/blob/9d24a6c1809a671c2e54c328897ebeae15a6d172/closure/goog/i18n/bidi.js#L203-L234 final UnicodePropertyLookup _textDirectionLookup = UnicodePropertyLookup(>[ // LTR const UnicodeRange(kChar_A, kChar_Z, ui.TextDirection.ltr), const UnicodeRange(kChar_a, kChar_z, ui.TextDirection.ltr), const UnicodeRange(0x00C0, 0x00D6, ui.TextDirection.ltr), const UnicodeRange(0x00D8, 0x00F6, ui.TextDirection.ltr), const UnicodeRange(0x00F8, 0x02B8, ui.TextDirection.ltr), const UnicodeRange(0x0300, 0x0590, ui.TextDirection.ltr), // RTL const UnicodeRange(0x0591, 0x06EF, ui.TextDirection.rtl), const UnicodeRange(0x06FA, 0x08FF, ui.TextDirection.rtl), // LTR const UnicodeRange(0x0900, 0x1FFF, ui.TextDirection.ltr), const UnicodeRange(0x200E, 0x200E, ui.TextDirection.ltr), // RTL const UnicodeRange(0x200F, 0x200F, ui.TextDirection.rtl), // LTR const UnicodeRange(0x2C00, 0xD801, ui.TextDirection.ltr), // RTL const UnicodeRange(0xD802, 0xD803, ui.TextDirection.rtl), // LTR const UnicodeRange(0xD804, 0xD839, ui.TextDirection.ltr), // RTL const UnicodeRange(0xD83A, 0xD83B, ui.TextDirection.rtl), // LTR const UnicodeRange(0xD83C, 0xDBFF, ui.TextDirection.ltr), const UnicodeRange(0xF900, 0xFB1C, ui.TextDirection.ltr), // RTL const UnicodeRange(0xFB1D, 0xFDFF, ui.TextDirection.rtl), // LTR const UnicodeRange(0xFE00, 0xFE6F, ui.TextDirection.ltr), // RTL const UnicodeRange(0xFE70, 0xFEFC, ui.TextDirection.rtl), // LTR const UnicodeRange(0xFEFD, 0xFFFF, ui.TextDirection.ltr), ], null); List _computeBidiFragments(String text) { final List fragments = []; if (text.isEmpty) { fragments.add(const BidiFragment(0, 0, null, FragmentFlow.previous)); return fragments; } int fragmentStart = 0; ui.TextDirection? textDirection = _getTextDirection(text, 0); FragmentFlow fragmentFlow = _getFragmentFlow(text, 0); for (int i = 1; i < text.length; i++) { final ui.TextDirection? charTextDirection = _getTextDirection(text, i); if (charTextDirection != textDirection) { // We've reached the end of a text direction fragment. fragments.add(BidiFragment(fragmentStart, i, textDirection, fragmentFlow)); fragmentStart = i; textDirection = charTextDirection; fragmentFlow = _getFragmentFlow(text, i); } else { // This code handles the case of a sequence of digits followed by a sequence // of LTR characters with no space in between. if (fragmentFlow == FragmentFlow.previous) { fragmentFlow = _getFragmentFlow(text, i); } } } fragments.add(BidiFragment(fragmentStart, text.length, textDirection, fragmentFlow)); return fragments; } ui.TextDirection? _getTextDirection(String text, int i) { final int codePoint = getCodePoint(text, i)!; if (_isDigit(codePoint) || _isMashriqiDigit(codePoint)) { // A sequence of regular digits or Mashriqi digits always goes from left to // regardless of their fragment flow direction. return ui.TextDirection.ltr; } final ui.TextDirection? textDirection = _textDirectionLookup.findForChar(codePoint); if (textDirection != null) { return textDirection; } return null; } FragmentFlow _getFragmentFlow(String text, int i) { final int codePoint = getCodePoint(text, i)!; if (_isDigit(codePoint)) { return FragmentFlow.previous; } if (_isMashriqiDigit(codePoint)) { return FragmentFlow.rtl; } final ui.TextDirection? textDirection = _textDirectionLookup.findForChar(codePoint); switch (textDirection) { case ui.TextDirection.ltr: return FragmentFlow.ltr; case ui.TextDirection.rtl: return FragmentFlow.rtl; case null: return FragmentFlow.sandwich; } } bool _isDigit(int codePoint) { return codePoint >= kChar_0 && codePoint <= kChar_9; } bool _isMashriqiDigit(int codePoint) { return codePoint >= kMashriqi_0 && codePoint <= kMashriqi_9; }