Skip to content

Commit c24ae18

Browse files
authored
[web] Use v8BreakIterator where possible (flutter#37317)
* [web] Use v8BreakIterator where possible * address review comments
1 parent 02cb789 commit c24ae18

File tree

4 files changed

+270
-47
lines changed

4 files changed

+270
-47
lines changed

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ extension DomWindowExtension on DomWindow {
6666
/// The Trusted Types API (when available).
6767
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API
6868
external DomTrustedTypePolicyFactory? get trustedTypes;
69+
70+
// ignore: non_constant_identifier_names
71+
external DomIntl get Intl;
6972
}
7073

7174
typedef DomRequestAnimationFrameCallback = void Function(num highResTime);
@@ -1659,3 +1662,42 @@ class _DomListWrapper<T> extends Iterable<T> {
16591662
/// `toList` on the `Iterable`.
16601663
Iterable<T> createDomListWrapper<T>(_DomList list) =>
16611664
_DomListWrapper<T>._(list).cast<T>();
1665+
1666+
@JS()
1667+
@staticInterop
1668+
class DomIntl {}
1669+
1670+
extension DomIntlExtension on DomIntl {
1671+
/// This is a V8-only API for segmenting text.
1672+
///
1673+
/// See: https://code.google.com/archive/p/v8-i18n/wikis/BreakIterator.wiki
1674+
external Object? get v8BreakIterator;
1675+
}
1676+
1677+
1678+
@JS()
1679+
@staticInterop
1680+
class DomV8BreakIterator {}
1681+
1682+
extension DomV8BreakIteratorExtension on DomV8BreakIterator {
1683+
external void adoptText(String text);
1684+
external int first();
1685+
external int next();
1686+
external int current();
1687+
external String breakType();
1688+
}
1689+
1690+
DomV8BreakIterator createV8BreakIterator() {
1691+
final Object? v8BreakIterator = domWindow.Intl.v8BreakIterator;
1692+
if (v8BreakIterator == null) {
1693+
throw UnimplementedError('v8BreakIterator is not supported.');
1694+
}
1695+
1696+
return js_util.callConstructor<DomV8BreakIterator>(
1697+
v8BreakIterator,
1698+
<Object?>[
1699+
js_util.getProperty(domWindow, 'undefined'),
1700+
js_util.jsify(const <String, String>{'type': 'line'}),
1701+
],
1702+
);
1703+
}

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

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import '../dom.dart';
56
import 'fragmenter.dart';
67
import 'line_break_properties.dart';
78
import 'unicode_range.dart';
89

10+
const Set<int> _kNewlines = <int>{
11+
0x000A, // LF
12+
0x000B, // BK
13+
0x000C, // BK
14+
0x000D, // CR
15+
0x0085, // NL
16+
0x2028, // BK
17+
0x2029, // BK
18+
};
19+
const Set<int> _kSpaces = <int>{
20+
0x0020, // SP
21+
0x200B, // ZW
22+
};
23+
924
/// Various types of line breaks as defined by the Unicode spec.
1025
enum LineBreakType {
1126
/// Indicates that a line break is possible but not mandatory.
@@ -25,15 +40,107 @@ enum LineBreakType {
2540
}
2641

2742
/// Splits [text] into fragments based on line breaks.
28-
class LineBreakFragmenter extends TextFragmenter {
29-
const LineBreakFragmenter(super.text);
43+
abstract class LineBreakFragmenter extends TextFragmenter {
44+
factory LineBreakFragmenter(String text) {
45+
if (domWindow.Intl.v8BreakIterator != null) {
46+
return V8LineBreakFragmenter(text);
47+
}
48+
return FWLineBreakFragmenter(text);
49+
}
50+
51+
@override
52+
List<LineBreakFragment> fragment();
53+
}
54+
55+
/// Flutter web's custom implementation of [LineBreakFragmenter].
56+
class FWLineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter {
57+
FWLineBreakFragmenter(super.text);
3058

3159
@override
3260
List<LineBreakFragment> fragment() {
3361
return _computeLineBreakFragments(text);
3462
}
3563
}
3664

65+
/// An implementation of [LineBreakFragmenter] that uses V8's
66+
/// `v8BreakIterator` API to find line breaks in the given [text].
67+
class V8LineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter {
68+
V8LineBreakFragmenter(super.text)
69+
: assert(domWindow.Intl.v8BreakIterator != null);
70+
71+
@override
72+
List<LineBreakFragment> fragment() {
73+
final List<LineBreakFragment> breaks = <LineBreakFragment>[];
74+
int fragmentStart = 0;
75+
76+
final DomV8BreakIterator iterator = createV8BreakIterator();
77+
78+
iterator.adoptText(text);
79+
iterator.first();
80+
while (iterator.next() != -1) {
81+
final LineBreakType type = _getBreakType(iterator);
82+
83+
final int fragmentEnd = iterator.current();
84+
int trailingNewlines = 0;
85+
int trailingSpaces = 0;
86+
87+
// Calculate trailing newlines and spaces.
88+
for (int i = fragmentStart; i < fragmentEnd; i++) {
89+
final int codeUnit = text.codeUnitAt(i);
90+
if (_kNewlines.contains(codeUnit)) {
91+
trailingNewlines++;
92+
trailingSpaces++;
93+
} else if (_kSpaces.contains(codeUnit)) {
94+
trailingSpaces++;
95+
} else {
96+
// Always break after a sequence of spaces.
97+
if (trailingSpaces > 0) {
98+
breaks.add(LineBreakFragment(
99+
fragmentStart,
100+
i,
101+
LineBreakType.opportunity,
102+
trailingNewlines: trailingNewlines,
103+
trailingSpaces: trailingSpaces,
104+
));
105+
fragmentStart = i;
106+
trailingNewlines = 0;
107+
trailingSpaces = 0;
108+
}
109+
}
110+
}
111+
112+
breaks.add(LineBreakFragment(
113+
fragmentStart,
114+
fragmentEnd,
115+
type,
116+
trailingNewlines: trailingNewlines,
117+
trailingSpaces: trailingSpaces,
118+
));
119+
fragmentStart = fragmentEnd;
120+
}
121+
122+
if (breaks.isEmpty || breaks.last.type == LineBreakType.mandatory) {
123+
breaks.add(LineBreakFragment(text.length, text.length, LineBreakType.endOfText, trailingNewlines: 0, trailingSpaces: 0));
124+
}
125+
126+
return breaks;
127+
}
128+
129+
/// Gets break type from v8BreakIterator.
130+
LineBreakType _getBreakType(DomV8BreakIterator iterator) {
131+
final int fragmentEnd = iterator.current();
132+
133+
// I don't know why v8BreakIterator uses the type "none" to mean "soft break".
134+
if (iterator.breakType() != 'none') {
135+
return LineBreakType.mandatory;
136+
}
137+
if (fragmentEnd == text.length) {
138+
return LineBreakType.endOfText;
139+
}
140+
return LineBreakType.opportunity;
141+
}
142+
}
143+
37144
class LineBreakFragment extends TextFragment {
38145
const LineBreakFragment(super.start, super.end, this.type, {
39146
required this.trailingNewlines,

lib/web_ui/test/text/line_breaker_test.dart

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ void main() {
1717
}
1818

1919
void testMain() {
20-
group('$LineBreakFragmenter', () {
20+
groupForEachFragmenter(({required bool isV8}) {
21+
List<Line> split(String text) {
22+
final LineBreakFragmenter fragmenter =
23+
isV8 ? V8LineBreakFragmenter(text) : FWLineBreakFragmenter(text);
24+
return <Line>[
25+
for (final LineBreakFragment fragment in fragmenter.fragment())
26+
Line.fromLineBreakFragment(text, fragment)
27+
];
28+
}
29+
2130
test('empty string', () {
2231
expect(split(''), <Line>[
2332
Line('', endOfText),
@@ -316,13 +325,15 @@ void testMain() {
316325
});
317326

318327
test('comprehensive test', () {
319-
final List<TestCase> testCollection =
320-
parseRawTestData(rawLineBreakTestData);
328+
final List<TestCase> testCollection = parseRawTestData(rawLineBreakTestData, isV8: isV8);
321329
for (int t = 0; t < testCollection.length; t++) {
322330
final TestCase testCase = testCollection[t];
323331

324332
final String text = testCase.toText();
325-
final List<LineBreakFragment> fragments = LineBreakFragmenter(text).fragment();
333+
final LineBreakFragmenter fragmenter = isV8
334+
? V8LineBreakFragmenter(text)
335+
: FWLineBreakFragmenter(text);
336+
final List<LineBreakFragment> fragments = fragmenter.fragment();
326337

327338
// `f` is the index in the `fragments` list.
328339
int f = 0;
@@ -401,6 +412,23 @@ void testMain() {
401412
});
402413
}
403414

415+
typedef CreateLineBreakFragmenter = LineBreakFragmenter Function(String text);
416+
typedef GroupBody = void Function({required bool isV8});
417+
418+
void groupForEachFragmenter(GroupBody callback) {
419+
group(
420+
'$FWLineBreakFragmenter',
421+
() => callback(isV8: false),
422+
);
423+
424+
if (domWindow.Intl.v8BreakIterator != null) {
425+
group(
426+
'$V8LineBreakFragmenter',
427+
() => callback(isV8: true),
428+
);
429+
}
430+
}
431+
404432
/// Holds information about how a line was split from a string.
405433
class Line {
406434
Line(this.text, this.breakType, {this.nl = 0, this.sp = 0});
@@ -447,10 +475,3 @@ class Line {
447475
return '"$escapedText" ($breakType, nl: $nl, sp: $sp)';
448476
}
449477
}
450-
451-
List<Line> split(String text) {
452-
return <Line>[
453-
for (final LineBreakFragment fragment in LineBreakFragmenter(text).fragment())
454-
Line.fromLineBreakFragment(text, fragment)
455-
];
456-
}

0 commit comments

Comments
 (0)