| // 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 TextBoundary boundary = matchState['textBoundary'] as TextBoundary; |
| final int 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 CharacterBoundary 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 String text = 'a❄︎c'; |
| const CharacterBoundary 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 String 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 = 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 LineBoundary 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 String textA= 'abcd efg hi\njklmno\npqrstuv'; |
| const ParagraphBoundary boundaryA = ParagraphBoundary(textA); |
| |
| // Position enclosed inside of paragraph, 'abcd efg h|i\n'. |
| const int 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 String textB = 'abcd efg hi\r\njklmno\npqrstuv'; |
| const ParagraphBoundary boundaryB = ParagraphBoundary(textB); |
| expect(boundaryB.getLeadingTextBoundaryAt(position), 0); |
| expect(boundaryB.getTrailingTextBoundaryAt(position), 13); |
| |
| const String 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 ParagraphBoundary boundaryF = ParagraphBoundary(textF); |
| const int positionF = 11; |
| expect(boundaryF.getLeadingTextBoundaryAt(positionF), 0); |
| expect(boundaryF.getTrailingTextBoundaryAt(positionF), 20); |
| }); |
| |
| test('works for consecutive line terminators involving CRLF', () { |
| const String 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 ParagraphBoundary boundaryI = ParagraphBoundary(textI); |
| const int positionI = 56;// \n at the end of the third line. |
| const int positionJ = 38;// t at beginning of third line. |
| const int 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 String 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 ParagraphBoundary boundaryI = ParagraphBoundary(textI); |
| const int positionI = 55;// \n at the end of the third line. |
| const int positionJ = 37;// t at beginning of third line. |
| const int 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 String 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 ParagraphBoundary boundaryH = ParagraphBoundary(textH); |
| const int 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 String 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 ParagraphBoundary boundaryG = ParagraphBoundary(textG); |
| const int positionG = 18; |
| expect(boundaryG.getLeadingTextBoundaryAt(positionG), 18); |
| expect(boundaryG.getTrailingTextBoundaryAt(positionG), 20); |
| }); |
| |
| test('works when position is between two CRLF', () { |
| const String textE = 'abcd efg hi\r\nhello\r\n\n'; |
| const ParagraphBoundary boundaryE = ParagraphBoundary(textE); |
| // Position enclosed inside of paragraph, 'abcd efg hi\r\nhello\r\n\n'. |
| const int 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 String textC = 'abcd efg hi\r\n\n\n\n\n\n\n\n\n\n\n\njklmno\npqrstuv'; |
| const ParagraphBoundary 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 int positionC = 18; |
| expect(boundaryC.getLeadingTextBoundaryAt(positionC), 18); |
| expect(boundaryC.getTrailingTextBoundaryAt(positionC), 19); |
| |
| const String textD = 'abcd efg hi\r\n\n\n\n'; |
| const ParagraphBoundary boundaryD = ParagraphBoundary(textD); |
| // Position enclosed inside of paragraph, 'abcd efg hi\r\n\n|\n\n'. |
| const int positionD = 14; |
| expect(boundaryD.getLeadingTextBoundaryAt(positionD), 14); |
| expect(boundaryD.getTrailingTextBoundaryAt(positionD), 15); |
| }); |
| }); |
| |
| test('document boundary works', () { |
| const String text = 'abcd efg hi\njklmno\npqrstuv'; |
| const DocumentBoundary 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(); |
| } |
| } |