Skip to content

Commit

Permalink
[web] Use v8BreakIterator where possible (flutter#37317)
Browse files Browse the repository at this point in the history
* [web] Use v8BreakIterator where possible

* address review comments
  • Loading branch information
mdebbar authored and schwa423 committed Nov 16, 2022
1 parent 4ec44cd commit 3c19606
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 47 deletions.
42 changes: 42 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ extension DomWindowExtension on DomWindow {
/// The Trusted Types API (when available).
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API
external DomTrustedTypePolicyFactory? get trustedTypes;

// ignore: non_constant_identifier_names
external DomIntl get Intl;
}

typedef DomRequestAnimationFrameCallback = void Function(num highResTime);
Expand Down Expand Up @@ -1659,3 +1662,42 @@ class _DomListWrapper<T> extends Iterable<T> {
/// `toList` on the `Iterable`.
Iterable<T> createDomListWrapper<T>(_DomList list) =>
_DomListWrapper<T>._(list).cast<T>();

@JS()
@staticInterop
class DomIntl {}

extension DomIntlExtension on DomIntl {
/// This is a V8-only API for segmenting text.
///
/// See: https://code.google.com/archive/p/v8-i18n/wikis/BreakIterator.wiki
external Object? get v8BreakIterator;
}


@JS()
@staticInterop
class DomV8BreakIterator {}

extension DomV8BreakIteratorExtension on DomV8BreakIterator {
external void adoptText(String text);
external int first();
external int next();
external int current();
external String breakType();
}

DomV8BreakIterator createV8BreakIterator() {
final Object? v8BreakIterator = domWindow.Intl.v8BreakIterator;
if (v8BreakIterator == null) {
throw UnimplementedError('v8BreakIterator is not supported.');
}

return js_util.callConstructor<DomV8BreakIterator>(
v8BreakIterator,
<Object?>[
js_util.getProperty(domWindow, 'undefined'),
js_util.jsify(const <String, String>{'type': 'line'}),
],
);
}
111 changes: 109 additions & 2 deletions lib/web_ui/lib/src/engine/text/line_breaker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../dom.dart';
import 'fragmenter.dart';
import 'line_break_properties.dart';
import 'unicode_range.dart';

const Set<int> _kNewlines = <int>{
0x000A, // LF
0x000B, // BK
0x000C, // BK
0x000D, // CR
0x0085, // NL
0x2028, // BK
0x2029, // BK
};
const Set<int> _kSpaces = <int>{
0x0020, // SP
0x200B, // ZW
};

/// Various types of line breaks as defined by the Unicode spec.
enum LineBreakType {
/// Indicates that a line break is possible but not mandatory.
Expand All @@ -25,15 +40,107 @@ enum LineBreakType {
}

/// Splits [text] into fragments based on line breaks.
class LineBreakFragmenter extends TextFragmenter {
const LineBreakFragmenter(super.text);
abstract class LineBreakFragmenter extends TextFragmenter {
factory LineBreakFragmenter(String text) {
if (domWindow.Intl.v8BreakIterator != null) {
return V8LineBreakFragmenter(text);
}
return FWLineBreakFragmenter(text);
}

@override
List<LineBreakFragment> fragment();
}

/// Flutter web's custom implementation of [LineBreakFragmenter].
class FWLineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter {
FWLineBreakFragmenter(super.text);

@override
List<LineBreakFragment> fragment() {
return _computeLineBreakFragments(text);
}
}

/// An implementation of [LineBreakFragmenter] that uses V8's
/// `v8BreakIterator` API to find line breaks in the given [text].
class V8LineBreakFragmenter extends TextFragmenter implements LineBreakFragmenter {
V8LineBreakFragmenter(super.text)
: assert(domWindow.Intl.v8BreakIterator != null);

@override
List<LineBreakFragment> fragment() {
final List<LineBreakFragment> breaks = <LineBreakFragment>[];
int fragmentStart = 0;

final DomV8BreakIterator iterator = createV8BreakIterator();

iterator.adoptText(text);
iterator.first();
while (iterator.next() != -1) {
final LineBreakType type = _getBreakType(iterator);

final int fragmentEnd = iterator.current();
int trailingNewlines = 0;
int trailingSpaces = 0;

// Calculate trailing newlines and spaces.
for (int i = fragmentStart; i < fragmentEnd; i++) {
final int codeUnit = text.codeUnitAt(i);
if (_kNewlines.contains(codeUnit)) {
trailingNewlines++;
trailingSpaces++;
} else if (_kSpaces.contains(codeUnit)) {
trailingSpaces++;
} else {
// Always break after a sequence of spaces.
if (trailingSpaces > 0) {
breaks.add(LineBreakFragment(
fragmentStart,
i,
LineBreakType.opportunity,
trailingNewlines: trailingNewlines,
trailingSpaces: trailingSpaces,
));
fragmentStart = i;
trailingNewlines = 0;
trailingSpaces = 0;
}
}
}

breaks.add(LineBreakFragment(
fragmentStart,
fragmentEnd,
type,
trailingNewlines: trailingNewlines,
trailingSpaces: trailingSpaces,
));
fragmentStart = fragmentEnd;
}

if (breaks.isEmpty || breaks.last.type == LineBreakType.mandatory) {
breaks.add(LineBreakFragment(text.length, text.length, LineBreakType.endOfText, trailingNewlines: 0, trailingSpaces: 0));
}

return breaks;
}

/// Gets break type from v8BreakIterator.
LineBreakType _getBreakType(DomV8BreakIterator iterator) {
final int fragmentEnd = iterator.current();

// I don't know why v8BreakIterator uses the type "none" to mean "soft break".
if (iterator.breakType() != 'none') {
return LineBreakType.mandatory;
}
if (fragmentEnd == text.length) {
return LineBreakType.endOfText;
}
return LineBreakType.opportunity;
}
}

class LineBreakFragment extends TextFragment {
const LineBreakFragment(super.start, super.end, this.type, {
required this.trailingNewlines,
Expand Down
43 changes: 32 additions & 11 deletions lib/web_ui/test/text/line_breaker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ void main() {
}

void testMain() {
group('$LineBreakFragmenter', () {
groupForEachFragmenter(({required bool isV8}) {
List<Line> split(String text) {
final LineBreakFragmenter fragmenter =
isV8 ? V8LineBreakFragmenter(text) : FWLineBreakFragmenter(text);
return <Line>[
for (final LineBreakFragment fragment in fragmenter.fragment())
Line.fromLineBreakFragment(text, fragment)
];
}

test('empty string', () {
expect(split(''), <Line>[
Line('', endOfText),
Expand Down Expand Up @@ -316,13 +325,15 @@ void testMain() {
});

test('comprehensive test', () {
final List<TestCase> testCollection =
parseRawTestData(rawLineBreakTestData);
final List<TestCase> testCollection = parseRawTestData(rawLineBreakTestData, isV8: isV8);
for (int t = 0; t < testCollection.length; t++) {
final TestCase testCase = testCollection[t];

final String text = testCase.toText();
final List<LineBreakFragment> fragments = LineBreakFragmenter(text).fragment();
final LineBreakFragmenter fragmenter = isV8
? V8LineBreakFragmenter(text)
: FWLineBreakFragmenter(text);
final List<LineBreakFragment> fragments = fragmenter.fragment();

// `f` is the index in the `fragments` list.
int f = 0;
Expand Down Expand Up @@ -401,6 +412,23 @@ void testMain() {
});
}

typedef CreateLineBreakFragmenter = LineBreakFragmenter Function(String text);
typedef GroupBody = void Function({required bool isV8});

void groupForEachFragmenter(GroupBody callback) {
group(
'$FWLineBreakFragmenter',
() => callback(isV8: false),
);

if (domWindow.Intl.v8BreakIterator != null) {
group(
'$V8LineBreakFragmenter',
() => callback(isV8: true),
);
}
}

/// Holds information about how a line was split from a string.
class Line {
Line(this.text, this.breakType, {this.nl = 0, this.sp = 0});
Expand Down Expand Up @@ -447,10 +475,3 @@ class Line {
return '"$escapedText" ($breakType, nl: $nl, sp: $sp)';
}
}

List<Line> split(String text) {
return <Line>[
for (final LineBreakFragment fragment in LineBreakFragmenter(text).fragment())
Line.fromLineBreakFragment(text, fragment)
];
}
Loading

0 comments on commit 3c19606

Please sign in to comment.