blob: 5fe0f3358825c8d1a6eba3c1e6996e4837213675 [file] [log] [blame]
// 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;
}