318 lines
12 KiB
Dart
318 lines
12 KiB
Dart
// Copyright 2014 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:flutter/rendering.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
class _ConsistentTextRangeImplementationMatcher extends Matcher {
|
||
_ConsistentTextRangeImplementationMatcher(int length)
|
||
: range = TextRange(start: -1, end: length + 1),
|
||
assert(length >= 0);
|
||
|
||
final TextRange range;
|
||
@override
|
||
Description describe(Description description) {
|
||
return description.add(
|
||
'The implementation of TextBoundary.getTextBoundaryAt is consistent with its other methods.',
|
||
);
|
||
}
|
||
|
||
@override
|
||
Description describeMismatch(
|
||
dynamic item,
|
||
Description mismatchDescription,
|
||
Map<dynamic, dynamic> matchState,
|
||
bool verbose,
|
||
) {
|
||
final boundary = matchState['textBoundary'] as TextBoundary;
|
||
final position = matchState['position'] as int;
|
||
final int leading = boundary.getLeadingTextBoundaryAt(position) ?? -1;
|
||
final int trailing = boundary.getTrailingTextBoundaryAt(position) ?? -1;
|
||
|
||
return mismatchDescription.add(
|
||
'at position $position, expected ${TextRange(start: leading, end: trailing)} but got ${boundary.getTextBoundaryAt(position)}',
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||
for (int i = range.start; i <= range.end; i++) {
|
||
final int? leading = (item as TextBoundary).getLeadingTextBoundaryAt(i);
|
||
final int? trailing = item.getTrailingTextBoundaryAt(i);
|
||
final TextRange boundary = item.getTextBoundaryAt(i);
|
||
final bool consistent = boundary.start == (leading ?? -1) && boundary.end == (trailing ?? -1);
|
||
if (!consistent) {
|
||
matchState['textBoundary'] = item;
|
||
matchState['position'] = i;
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
|
||
Matcher _hasConsistentTextRangeImplementationWithinRange(int length) =>
|
||
_ConsistentTextRangeImplementationMatcher(length);
|
||
|
||
void main() {
|
||
test('Character boundary works', () {
|
||
const boundary = CharacterBoundary('abc');
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(3));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), 0);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), 1);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(1), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(1), 2);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(2), 2);
|
||
expect(boundary.getTrailingTextBoundaryAt(2), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(3), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), null);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(4), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(4), null);
|
||
});
|
||
|
||
test('Character boundary works with grapheme', () {
|
||
const text = 'a❄︎c';
|
||
const boundary = CharacterBoundary(text);
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(text.length));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), 0);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), 1);
|
||
|
||
// The `❄` takes two character length.
|
||
expect(boundary.getLeadingTextBoundaryAt(1), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(1), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(2), 1);
|
||
expect(boundary.getTrailingTextBoundaryAt(2), 3);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(3), 3);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), 4);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length), text.length);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length), null);
|
||
});
|
||
|
||
test('wordBoundary.moveByWordBoundary', () {
|
||
const text =
|
||
'ABC ABC\n' // [0, 10)
|
||
'AÁ Á\n' // [10, 20)
|
||
' \n' // [20, 30)
|
||
'ABC!!!ABC\n' // [30, 40)
|
||
' !ABC !!\n' // [40, 50)
|
||
'A 𑗋𑗋 A\n'; // [50, 60)
|
||
|
||
final textPainter = TextPainter()
|
||
..textDirection = TextDirection.ltr
|
||
..text = const TextSpan(text: text)
|
||
..layout();
|
||
|
||
final TextBoundary boundary = textPainter.wordBoundaries.moveByWordBoundary;
|
||
|
||
// 4 points to the 2nd whitespace in the first line.
|
||
// Don't break between horizontal spaces and letters/numbers.
|
||
expect(boundary.getLeadingTextBoundaryAt(4), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(4), 9);
|
||
|
||
// Works when words are starting/ending with a combining diacritical mark.
|
||
expect(boundary.getLeadingTextBoundaryAt(14), 10);
|
||
expect(boundary.getTrailingTextBoundaryAt(14), 19);
|
||
|
||
// Do break before and after newlines.
|
||
expect(boundary.getLeadingTextBoundaryAt(24), 20);
|
||
expect(boundary.getTrailingTextBoundaryAt(24), 29);
|
||
|
||
// Do not break on punctuations.
|
||
expect(boundary.getLeadingTextBoundaryAt(34), 30);
|
||
expect(boundary.getTrailingTextBoundaryAt(34), 39);
|
||
|
||
// Ok to break if next to punctuations or separating spaces.
|
||
expect(boundary.getLeadingTextBoundaryAt(44), 43);
|
||
expect(boundary.getTrailingTextBoundaryAt(44), 46);
|
||
|
||
// 44 points to a low surrogate of a punctuation.
|
||
expect(boundary.getLeadingTextBoundaryAt(54), 50);
|
||
expect(boundary.getTrailingTextBoundaryAt(54), 59);
|
||
});
|
||
|
||
test('line boundary works', () {
|
||
final boundary = LineBoundary(TestTextLayoutMetrics());
|
||
expect(boundary.getLeadingTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3.start);
|
||
expect(boundary.getTrailingTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3.end);
|
||
expect(boundary.getTextBoundaryAt(3), TestTextLayoutMetrics.lineAt3);
|
||
});
|
||
|
||
group('paragraph boundary', () {
|
||
test('works for simple cases', () {
|
||
const textA = 'abcd efg hi\njklmno\npqrstuv';
|
||
const boundaryA = ParagraphBoundary(textA);
|
||
|
||
// Position enclosed inside of paragraph, 'abcd efg h|i\n'.
|
||
const position = 10;
|
||
|
||
// The range includes the line terminator.
|
||
expect(boundaryA.getLeadingTextBoundaryAt(position), 0);
|
||
expect(boundaryA.getTrailingTextBoundaryAt(position), 12);
|
||
|
||
// This text includes a carriage return followed by a line feed.
|
||
const textB = 'abcd efg hi\r\njklmno\npqrstuv';
|
||
const boundaryB = ParagraphBoundary(textB);
|
||
expect(boundaryB.getLeadingTextBoundaryAt(position), 0);
|
||
expect(boundaryB.getTrailingTextBoundaryAt(position), 13);
|
||
|
||
const textF =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n' // 20 + 16 => 36
|
||
'to come to the aid\n' // 36 + 19 => 55
|
||
'of their country.'; // 55 + 17 => 72
|
||
const boundaryF = ParagraphBoundary(textF);
|
||
const positionF = 11;
|
||
expect(boundaryF.getLeadingTextBoundaryAt(positionF), 0);
|
||
expect(boundaryF.getTrailingTextBoundaryAt(positionF), 20);
|
||
});
|
||
|
||
test('works for consecutive line terminators involving CRLF', () {
|
||
const textI =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n\r\n' // 20 + 16 => 38
|
||
'to come to the aid\n' // 38 + 19 => 57
|
||
'of their country.'; // 57 + 17 => 74
|
||
const boundaryI = ParagraphBoundary(textI);
|
||
const positionI = 56; // \n at the end of the third line.
|
||
const positionJ = 38; // t at beginning of third line.
|
||
const positionK = 37; // \n at end of second line.
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionI), 38);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionI), 57);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionJ), 38);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionJ), 57);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionK), 36);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionK), 38);
|
||
});
|
||
|
||
test('works for consecutive line terminators', () {
|
||
const textI =
|
||
'Now is the time for\n' // 20
|
||
'all good people\n\n' // 20 + 16 => 37
|
||
'to come to the aid\n' // 37 + 19 => 56
|
||
'of their country.'; // 56 + 17 => 73
|
||
const boundaryI = ParagraphBoundary(textI);
|
||
const positionI = 55; // \n at the end of the third line.
|
||
const positionJ = 37; // t at beginning of third line.
|
||
const positionK = 36; // \n at end of second line.
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionI), 37);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionI), 56);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionJ), 37);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionJ), 56);
|
||
expect(boundaryI.getLeadingTextBoundaryAt(positionK), 36);
|
||
expect(boundaryI.getTrailingTextBoundaryAt(positionK), 37);
|
||
});
|
||
|
||
test('leading boundary works for consecutive CRLF', () {
|
||
// This text includes multiple consecutive carriage returns followed by line feeds (CRLF).
|
||
const textH = 'abcd efg hi\r\n\r\n\r\n\r\n\r\n\r\n\r\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryH = ParagraphBoundary(textH);
|
||
const positionH = 18;
|
||
expect(boundaryH.getLeadingTextBoundaryAt(positionH), 17);
|
||
expect(boundaryH.getTrailingTextBoundaryAt(positionH), 19);
|
||
});
|
||
|
||
test('trailing boundary works for consecutive CRLF', () {
|
||
// This text includes multiple consecutive carriage returns followed by line feeds (CRLF).
|
||
const textG = 'abcd efg hi\r\n\n\n\n\n\n\r\n\r\n\r\n\r\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryG = ParagraphBoundary(textG);
|
||
const positionG = 18;
|
||
expect(boundaryG.getLeadingTextBoundaryAt(positionG), 18);
|
||
expect(boundaryG.getTrailingTextBoundaryAt(positionG), 20);
|
||
});
|
||
|
||
test('works when position is between two CRLF', () {
|
||
const textE = 'abcd efg hi\r\nhello\r\n\n';
|
||
const boundaryE = ParagraphBoundary(textE);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\nhello\r\n\n'.
|
||
const positionE = 16;
|
||
expect(boundaryE.getLeadingTextBoundaryAt(positionE), 13);
|
||
expect(boundaryE.getTrailingTextBoundaryAt(positionE), 20);
|
||
});
|
||
|
||
test('works for multiple consecutive line terminators', () {
|
||
// This text includes multiple consecutive line terminators.
|
||
const textC = 'abcd efg hi\r\n\n\n\n\n\n\n\n\n\n\n\njklmno\npqrstuv';
|
||
const boundaryC = ParagraphBoundary(textC);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\n\n\n\n\n\n|\n\n\n\n\n\njklmno\npqrstuv'.
|
||
const positionC = 18;
|
||
expect(boundaryC.getLeadingTextBoundaryAt(positionC), 18);
|
||
expect(boundaryC.getTrailingTextBoundaryAt(positionC), 19);
|
||
|
||
const textD = 'abcd efg hi\r\n\n\n\n';
|
||
const boundaryD = ParagraphBoundary(textD);
|
||
// Position enclosed inside of paragraph, 'abcd efg hi\r\n\n|\n\n'.
|
||
const positionD = 14;
|
||
expect(boundaryD.getLeadingTextBoundaryAt(positionD), 14);
|
||
expect(boundaryD.getTrailingTextBoundaryAt(positionD), 15);
|
||
});
|
||
});
|
||
|
||
test('document boundary works', () {
|
||
const text = 'abcd efg hi\njklmno\npqrstuv';
|
||
const boundary = DocumentBoundary(text);
|
||
expect(boundary, _hasConsistentTextRangeImplementationWithinRange(text.length));
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(-1), null);
|
||
expect(boundary.getTrailingTextBoundaryAt(-1), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(0), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(0), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(10), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(10), text.length);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length), null);
|
||
|
||
expect(boundary.getLeadingTextBoundaryAt(text.length + 1), 0);
|
||
expect(boundary.getTrailingTextBoundaryAt(text.length + 1), null);
|
||
});
|
||
}
|
||
|
||
class TestTextLayoutMetrics extends TextLayoutMetrics {
|
||
static const TextSelection lineAt3 = TextSelection(baseOffset: 0, extentOffset: 10);
|
||
static const TextRange wordBoundaryAt3 = TextRange(start: 4, end: 7);
|
||
|
||
@override
|
||
TextSelection getLineAtOffset(TextPosition position) {
|
||
if (position.offset == 3) {
|
||
return lineAt3;
|
||
}
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextPosition getTextPositionAbove(TextPosition position) {
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextPosition getTextPositionBelow(TextPosition position) {
|
||
throw UnimplementedError();
|
||
}
|
||
|
||
@override
|
||
TextRange getWordBoundary(TextPosition position) {
|
||
if (position.offset == 3) {
|
||
return wordBoundaryAt3;
|
||
}
|
||
throw UnimplementedError();
|
||
}
|
||
}
|