Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 7b13622

Browse files
committed
[web] Bidi fragmenter
1 parent 32b8bd5 commit 7b13622

File tree

2 files changed

+148
-134
lines changed

2 files changed

+148
-134
lines changed

lib/web_ui/lib/src/engine/text/text_direction.dart

Lines changed: 87 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,41 @@
44

55
import 'package:ui/ui.dart' as ui;
66

7+
import 'fragmenter.dart';
78
import 'line_breaker.dart';
89
import 'unicode_range.dart';
910

11+
class BidiFragmenter extends TextFragmenter {
12+
const BidiFragmenter(super.paragraph);
13+
14+
@override
15+
List<BidiFragment> fragment() {
16+
return _computeBidiFragments(paragraph.toPlainText(), paragraph.paragraphStyle.effectiveTextDirection);
17+
}
18+
}
19+
20+
class BidiFragment extends TextFragment {
21+
const BidiFragment(super.start, super.end, this.textDirection);
22+
23+
final ui.TextDirection textDirection;
24+
25+
@override
26+
int get hashCode => Object.hash(start, end, textDirection);
27+
28+
@override
29+
bool operator ==(Object other) {
30+
return other is BidiFragment &&
31+
other.start == start &&
32+
other.end == end &&
33+
other.textDirection == textDirection;
34+
}
35+
36+
@override
37+
String toString() {
38+
return 'BidiFragment($start, $end, $textDirection)';
39+
}
40+
}
41+
1042
// This data was taken from the source code of the Closure library:
1143
//
1244
// - https://github.com/google/closure-library/blob/9d24a6c1809a671c2e54c328897ebeae15a6d172/closure/goog/i18n/bidi.js#L203-L234
@@ -75,44 +107,64 @@ class DirectionalPosition {
75107
}
76108
}
77109

78-
/// Finds the end of the directional block of text that starts at [start] up
79-
/// until [end].
80-
///
81-
/// If the block goes beyond [end], the part after [end] is ignored.
82-
DirectionalPosition getDirectionalBlockEnd(
83-
String text,
84-
LineBreakResult start,
85-
LineBreakResult end,
86-
) {
87-
if (start.index == end.index) {
88-
return DirectionalPosition(end, null, false);
89-
}
110+
List<BidiFragment> _computeBidiFragments(String text, ui.TextDirection baseDirection) {
111+
final List<BidiFragment> fragments = <BidiFragment>[];
90112

91-
// Check if we are in a space-only block.
92-
if (start.index == end.indexWithoutTrailingSpaces) {
93-
return DirectionalPosition(end, null, true);
94-
}
113+
int fragmentStart = 0;
114+
ui.TextDirection fragmentDirection = _textDirectionLookup.find(text, 0) ?? baseDirection;
95115

96-
final ui.TextDirection? blockDirection = _textDirectionLookup.find(text, start.index);
97-
int i = start.index + 1;
98-
99-
while (i < end.indexWithoutTrailingSpaces) {
100-
final ui.TextDirection? direction = _textDirectionLookup.find(text, i);
101-
if (direction != blockDirection) {
102-
// Reached the next block.
103-
break;
116+
for (int i = 1; i < text.length; i++) {
117+
final ui.TextDirection charDirection = _textDirectionLookup.find(text, i) ?? fragmentDirection;
118+
if (charDirection != fragmentDirection) {
119+
// We've reached the end of a text direction fragment.
120+
fragments.add(BidiFragment(fragmentStart, i, fragmentDirection));
121+
fragmentStart = i;
122+
fragmentDirection = charDirection;
104123
}
105-
i++;
106124
}
107125

108-
if (i == end.indexWithoutTrailingNewlines) {
109-
// If all that remains before [end] is new lines, let's include them in the
110-
// block.
111-
return DirectionalPosition(end, blockDirection, false);
112-
}
113-
return DirectionalPosition(
114-
LineBreakResult.sameIndex(i, LineBreakType.prohibited),
115-
blockDirection,
116-
false,
117-
);
126+
fragments.add(BidiFragment(fragmentStart, text.length, fragmentDirection));
127+
return fragments;
118128
}
129+
130+
// /// Finds the end of the directional block of text that starts at [start] up
131+
// /// until [end].
132+
// ///
133+
// /// If the block goes beyond [end], the part after [end] is ignored.
134+
// DirectionalPosition _getDirectionalBlockEnd(
135+
// String text,
136+
// LineBreakResult start,
137+
// LineBreakResult end,
138+
// ) {
139+
// if (start.index == end.index) {
140+
// return DirectionalPosition(end, null, false);
141+
// }
142+
143+
// // Check if we are in a space-only block.
144+
// if (start.index == end.indexWithoutTrailingSpaces) {
145+
// return DirectionalPosition(end, null, true);
146+
// }
147+
148+
// final ui.TextDirection? blockDirection = _textDirectionLookup.find(text, start.index);
149+
// int i = start.index + 1;
150+
151+
// while (i < end.indexWithoutTrailingSpaces) {
152+
// final ui.TextDirection? direction = _textDirectionLookup.find(text, i);
153+
// if (direction != blockDirection) {
154+
// // Reached the next block.
155+
// break;
156+
// }
157+
// i++;
158+
// }
159+
160+
// if (i == end.indexWithoutTrailingNewlines) {
161+
// // If all that remains before [end] is new lines, let's include them in the
162+
// // block.
163+
// return DirectionalPosition(end, blockDirection, false);
164+
// }
165+
// return DirectionalPosition(
166+
// LineBreakResult.sameIndex(i, LineBreakType.prohibited),
167+
// blockDirection,
168+
// false,
169+
// );
170+
// }

lib/web_ui/test/text/text_direction_test.dart

Lines changed: 61 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:test/test.dart';
88
import 'package:ui/src/engine.dart';
99
import 'package:ui/ui.dart';
1010

11+
import '../html/paragraph/helper.dart';
12+
1113
// Two RTL strings, 5 characters each, to match the length of "$rtl1" and "$rtl2".
1214
const String rtl1 = 'واحدة';
1315
const String rtl2 = 'ثنتان';
@@ -17,110 +19,70 @@ void main() {
1719
}
1820

1921
Future<void> testMain() async {
20-
group('$getDirectionalBlockEnd', () {
22+
group('$BidiFragmenter', () {
2123

2224
test('basic cases', () {
23-
const String text = 'Lorem 12 $rtl1 ipsum34';
24-
const LineBreakResult start = LineBreakResult.sameIndex(0, LineBreakType.prohibited);
25-
const LineBreakResult end = LineBreakResult.sameIndex(text.length, LineBreakType.endOfText);
26-
const LineBreakResult loremMiddle = LineBreakResult.sameIndex(3, LineBreakType.prohibited);
27-
const LineBreakResult loremEnd = LineBreakResult.sameIndex(5, LineBreakType.prohibited);
28-
const LineBreakResult twelveStart = LineBreakResult(6, 6, 5, LineBreakType.opportunity);
29-
const LineBreakResult twelveEnd = LineBreakResult.sameIndex(8, LineBreakType.prohibited);
30-
const LineBreakResult rtl1Start = LineBreakResult(9, 9, 8, LineBreakType.opportunity);
31-
const LineBreakResult rtl1End = LineBreakResult.sameIndex(14, LineBreakType.prohibited);
32-
const LineBreakResult ipsumStart = LineBreakResult(17, 17, 15, LineBreakType.opportunity);
33-
const LineBreakResult ipsumEnd = LineBreakResult.sameIndex(22, LineBreakType.prohibited);
34-
35-
DirectionalPosition blockEnd;
36-
37-
blockEnd = getDirectionalBlockEnd(text, start, end);
38-
expect(blockEnd.isSpaceOnly, isFalse);
39-
expect(blockEnd.textDirection, TextDirection.ltr);
40-
expect(blockEnd.lineBreak, loremEnd);
41-
42-
blockEnd = getDirectionalBlockEnd(text, start, loremMiddle);
43-
expect(blockEnd.isSpaceOnly, isFalse);
44-
expect(blockEnd.textDirection, TextDirection.ltr);
45-
expect(blockEnd.lineBreak, loremMiddle);
46-
47-
blockEnd = getDirectionalBlockEnd(text, loremMiddle, loremEnd);
48-
expect(blockEnd.isSpaceOnly, isFalse);
49-
expect(blockEnd.textDirection, TextDirection.ltr);
50-
expect(blockEnd.lineBreak, loremEnd);
51-
52-
blockEnd = getDirectionalBlockEnd(text, loremEnd, twelveStart);
53-
expect(blockEnd.isSpaceOnly, isTrue);
54-
expect(blockEnd.textDirection, isNull);
55-
expect(blockEnd.lineBreak, twelveStart);
56-
57-
blockEnd = getDirectionalBlockEnd(text, twelveStart, rtl1Start);
58-
expect(blockEnd.isSpaceOnly, isFalse);
59-
expect(blockEnd.textDirection, isNull);
60-
expect(blockEnd.lineBreak, twelveEnd);
61-
62-
blockEnd = getDirectionalBlockEnd(text, rtl1Start, end);
63-
expect(blockEnd.isSpaceOnly, isFalse);
64-
expect(blockEnd.textDirection, TextDirection.rtl);
65-
expect(blockEnd.lineBreak, rtl1End);
66-
67-
blockEnd = getDirectionalBlockEnd(text, ipsumStart, end);
68-
expect(blockEnd.isSpaceOnly, isFalse);
69-
expect(blockEnd.textDirection, TextDirection.ltr);
70-
expect(blockEnd.lineBreak, ipsumEnd);
71-
72-
blockEnd = getDirectionalBlockEnd(text, ipsumEnd, end);
73-
expect(blockEnd.isSpaceOnly, isFalse);
74-
expect(blockEnd.textDirection, isNull);
75-
expect(blockEnd.lineBreak, end);
25+
expect(split('Lorem 12 $rtl1 ipsum34'), <_Bidi>[
26+
_Bidi('Lorem 12 ', TextDirection.ltr),
27+
_Bidi('$rtl1 ', TextDirection.rtl),
28+
_Bidi('ipsum34', TextDirection.ltr),
29+
]);
30+
});
31+
32+
test('symbols', () {
33+
expect(split('Calculate 2.2 + 4.5 and write the result'), <_Bidi>[
34+
_Bidi('Calculate 2.2 + 4.5 and write the result', TextDirection.ltr),
35+
]);
36+
37+
expect(split('Calculate $rtl1 2.2 + 4.5 and write the result'), <_Bidi>[
38+
_Bidi('Calculate ', TextDirection.ltr),
39+
_Bidi('$rtl1 2.2 + 4.5 ', TextDirection.rtl),
40+
_Bidi('and write the result', TextDirection.ltr),
41+
]);
7642
});
7743

7844
test('handles new lines', () {
79-
const String text = 'Lorem\n12\nipsum \n';
80-
const LineBreakResult start = LineBreakResult.sameIndex(0, LineBreakType.prohibited);
81-
const LineBreakResult end = LineBreakResult(
82-
text.length,
83-
text.length - 1,
84-
text.length - 3,
85-
LineBreakType.mandatory,
86-
);
87-
const LineBreakResult loremEnd = LineBreakResult.sameIndex(5, LineBreakType.prohibited);
88-
const LineBreakResult twelveStart = LineBreakResult(6, 5, 5, LineBreakType.mandatory);
89-
const LineBreakResult twelveEnd = LineBreakResult.sameIndex(8, LineBreakType.prohibited);
90-
const LineBreakResult ipsumStart = LineBreakResult(9, 8, 8, LineBreakType.mandatory);
91-
const LineBreakResult ipsumEnd = LineBreakResult.sameIndex(14, LineBreakType.prohibited);
92-
93-
DirectionalPosition blockEnd;
94-
95-
blockEnd = getDirectionalBlockEnd(text, start, twelveStart);
96-
expect(blockEnd.isSpaceOnly, isFalse);
97-
expect(blockEnd.textDirection, TextDirection.ltr);
98-
expect(blockEnd.lineBreak, twelveStart);
99-
100-
blockEnd = getDirectionalBlockEnd(text, loremEnd, twelveStart);
101-
expect(blockEnd.isSpaceOnly, isTrue);
102-
expect(blockEnd.textDirection, isNull);
103-
expect(blockEnd.lineBreak, twelveStart);
104-
105-
blockEnd = getDirectionalBlockEnd(text, twelveStart, ipsumStart);
106-
expect(blockEnd.isSpaceOnly, isFalse);
107-
expect(blockEnd.textDirection, isNull);
108-
expect(blockEnd.lineBreak, ipsumStart);
109-
110-
blockEnd = getDirectionalBlockEnd(text, twelveEnd, ipsumStart);
111-
expect(blockEnd.isSpaceOnly, isTrue);
112-
expect(blockEnd.textDirection, isNull);
113-
expect(blockEnd.lineBreak, ipsumStart);
114-
115-
blockEnd = getDirectionalBlockEnd(text, ipsumStart, end);
116-
expect(blockEnd.isSpaceOnly, isFalse);
117-
expect(blockEnd.textDirection, TextDirection.ltr);
118-
expect(blockEnd.lineBreak, ipsumEnd);
119-
120-
blockEnd = getDirectionalBlockEnd(text, ipsumEnd, end);
121-
expect(blockEnd.isSpaceOnly, isTrue);
122-
expect(blockEnd.textDirection, isNull);
123-
expect(blockEnd.lineBreak, end);
45+
expect(split('Lorem\n12\nipsum \n'), <_Bidi>[
46+
_Bidi('Lorem\n12\nipsum \n', TextDirection.ltr),
47+
]);
48+
49+
expect(split('$rtl1\n $rtl2 \n'), <_Bidi>[
50+
_Bidi('$rtl1\n $rtl2 \n', TextDirection.rtl),
51+
]);
12452
});
12553
});
12654
}
55+
56+
/// Holds information about how a bidi region was split from a string.
57+
class _Bidi {
58+
_Bidi(this.text, this.textDirection);
59+
60+
final String text;
61+
final TextDirection textDirection;
62+
63+
@override
64+
int get hashCode => Object.hash(text, textDirection);
65+
66+
@override
67+
bool operator ==(Object other) {
68+
return other is _Bidi && other.text == text && other.textDirection == textDirection;
69+
}
70+
71+
@override
72+
String toString() {
73+
return '"$text" ($textDirection)';
74+
}
75+
}
76+
77+
List<_Bidi> split(String text) {
78+
return <_Bidi>[
79+
for (final BidiFragment fragment in computeBidiFragments(text))
80+
_Bidi(text.substring(fragment.start, fragment.end), fragment.textDirection)
81+
];
82+
}
83+
84+
List<BidiFragment> computeBidiFragments(String text) {
85+
final CanvasParagraph paragraph = plain(EngineParagraphStyle(), text);
86+
final BidiFragmenter fragmenter = BidiFragmenter(paragraph);
87+
return fragmenter.fragment();
88+
}

0 commit comments

Comments
 (0)