| // 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. |
| |
| import 'package:ui/ui.dart' as ui; |
| |
| import 'fragmenter.dart'; |
| import 'unicode_range.dart'; |
| |
| 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<BidiFragment> 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<ui.TextDirection?> _textDirectionLookup = UnicodePropertyLookup<ui.TextDirection?>( |
| <UnicodeRange<ui.TextDirection>>[ |
| // LTR |
| const UnicodeRange<ui.TextDirection>(kChar_A, kChar_Z, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(kChar_a, kChar_z, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0x00C0, 0x00D6, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0x00D8, 0x00F6, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0x00F8, 0x02B8, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0x0300, 0x0590, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0x0591, 0x06EF, ui.TextDirection.rtl), |
| const UnicodeRange<ui.TextDirection>(0x06FA, 0x08FF, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0x0900, 0x1FFF, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0x200E, 0x200E, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0x200F, 0x200F, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0x2C00, 0xD801, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0xD802, 0xD803, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0xD804, 0xD839, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0xD83A, 0xD83B, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0xD83C, 0xDBFF, ui.TextDirection.ltr), |
| const UnicodeRange<ui.TextDirection>(0xF900, 0xFB1C, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0xFB1D, 0xFDFF, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0xFE00, 0xFE6F, ui.TextDirection.ltr), |
| // RTL |
| const UnicodeRange<ui.TextDirection>(0xFE70, 0xFEFC, ui.TextDirection.rtl), |
| // LTR |
| const UnicodeRange<ui.TextDirection>(0xFEFD, 0xFFFF, ui.TextDirection.ltr), |
| ], |
| null, |
| ); |
| |
| List<BidiFragment> _computeBidiFragments(String text) { |
| final List<BidiFragment> fragments = <BidiFragment>[]; |
| |
| 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; |
| } |