Line data Source code
1 : // Copyright 2026 The Authors. See the AUTHORS file for details.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'package:characters/characters.dart';
16 :
17 : extension StringExtension on String {
18 : // Matches a single Unicode whitespace character: tab/LF/FF/CR plus Zs category.
19 3 : static final _whitespaceChar = RegExp(r'^[\t\n\f\r\p{Zs}]$', unicode: true);
20 :
21 : // Matches a single Unicode punctuation or symbol character (CommonMark definition).
22 3 : static final _punctuationChar = RegExp(r'^[\p{P}\p{S}]$', unicode: true);
23 :
24 : /// True if all characters match the given [test].
25 1 : bool all(bool Function(String element) test) {
26 2 : for (final c in characters) {
27 1 : if (!test(c)) {
28 : return false;
29 : }
30 : }
31 : return true;
32 : }
33 :
34 : /// True if the string is entirely made up of whitespace characters, False otherwise.
35 1 : bool get isWhitespace =>
36 5 : isNotEmpty && all((s) => _whitespaceChar.hasMatch(s));
37 :
38 : /// True if the string is not entirely made up of whitespace characters, False otherwise.
39 2 : bool get isNotWhitespace => !isWhitespace;
40 :
41 : /// True if there is whitespace in the string, False otherwise.
42 1 : bool get hasWhitespace =>
43 6 : isNotEmpty && characters.any((s) => _whitespaceChar.hasMatch(s));
44 :
45 : /// True if there is no whitespace in the string, False otherwise.
46 2 : bool get hasNoWhitespace => !hasWhitespace;
47 :
48 : /// True if the string has 1 character and it's a CR or LF.
49 1 : bool get isEolChar {
50 5 : return codeUnits.length == 1 && lineEnding.contains(codeUnitAt(0));
51 : }
52 :
53 3 : bool get isEol => isLineFeed || isCrLf;
54 :
55 : /// True if the string has 1 character and it's a CR.
56 3 : bool get isCarriageReturn => codeUnitAt(0) == carriageReturn;
57 :
58 : /// True if the string has 1 character and it's a LF.
59 3 : bool get isLineFeed => codeUnitAt(0) == lineFeed;
60 :
61 2 : bool get isCrLf => this == '\r\n';
62 :
63 : /// True if the string has 1 character and it's an ASCII control character.
64 1 : bool get isAsciiControlCharacter =>
65 1 : isNotEmpty &&
66 3 : codeUnits.length == 1 &&
67 4 : (_between(codeUnitAt(0), 0x0000, 0x001F) || codeUnitAt(0) == 0x007F);
68 :
69 1 : bool get isAsciiPunctuationCharacter =>
70 1 : isNotEmpty &&
71 3 : codeUnits.length == 1 &&
72 2 : (_between(codeUnitAt(0), 0x21, 0x2F) ||
73 2 : _between(codeUnitAt(0), 0x3A, 0x40) ||
74 2 : _between(codeUnitAt(0), 0x5B, 0x60) ||
75 2 : _between(codeUnitAt(0), 0x7B, 0x7E));
76 :
77 1 : bool get isAsciiLetter =>
78 1 : isNotEmpty &&
79 3 : codeUnits.length == 1 &&
80 2 : (_between(codeUnitAt(0), 0x41, 0x5A) ||
81 2 : _between(codeUnitAt(0), 0x61, 0x7A));
82 :
83 1 : bool get isAsciiLetterUppercase =>
84 1 : isNotEmpty &&
85 3 : codeUnits.length == 1 &&
86 2 : (_between(codeUnitAt(0), 0x41, 0x5A));
87 :
88 1 : bool get isAsciiLetterLowercase =>
89 1 : isNotEmpty &&
90 3 : codeUnits.length == 1 &&
91 2 : (_between(codeUnitAt(0), 0x61, 0x7A));
92 :
93 1 : bool get isAsciiDigit =>
94 1 : isNotEmpty &&
95 3 : codeUnits.length == 1 &&
96 2 : (_between(codeUnitAt(0), 0x30, 0x39));
97 :
98 1 : bool get isUnicodePunctuationCharacter =>
99 3 : isNotEmpty && _punctuationChar.hasMatch(this);
100 :
101 3 : bool _between(int value, int min, int max) => value >= min && value <= max;
102 :
103 : /// cut using first instance of [separator],
104 : /// returning the `before` and `after` parts
105 : ///
106 : /// `found` is true if [separator] was found
107 1 : (String before, String after, bool found) cutFirst(String separator) {
108 2 : if (isEmpty || separator.isEmpty) {
109 : return (this, '', false);
110 : }
111 :
112 3 : var rng = characters.findFirst(separator.characters);
113 :
114 : if (rng == null) {
115 : return (this, '', false);
116 : }
117 2 : var before = rng.charactersBefore.toString();
118 2 : var after = rng.charactersAfter.toString();
119 : return (before, after, true);
120 : }
121 :
122 : /// Clip the [n] rightmost characters
123 1 : String clipRight(int n) {
124 1 : if (n <= 0) {
125 : return this;
126 : }
127 :
128 3 : var count = characters.length - n;
129 1 : if (count <= 0) {
130 : return '';
131 : }
132 :
133 3 : return characters.take(count).toString();
134 : }
135 :
136 1 : String toUpperCaseFirstLetter() {
137 1 : if (isEmpty) {
138 : return '';
139 : }
140 6 : return '${characters.first.toUpperCase()}${characters.getRange(1)}';
141 : }
142 : }
143 :
144 : /// `<control>` - LINE FEED (LF)
145 : const lineFeed = 0x000A;
146 :
147 : /// `<control>` - CARRIAGE RETURN (CR)
148 : const carriageReturn = 0x000D;
149 :
150 : const lineEnding = [lineFeed, carriageReturn];
|