forked from google/quiver-dart
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pattern.dart
142 lines (123 loc) · 4.41 KB
/
pattern.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// This library contains utilities for working with [RegExp]s and other
/// [Pattern]s.
library quiver.pattern;
// From the PatternCharacter rule here:
// http://ecma-international.org/ecma-262/5.1/#sec-15.10
final _specialChars = RegExp(r'([\\\^\$\.\|\+\[\]\(\)\{\}])');
/// Escapes special regex characters in [str] so that it can be used as a
/// literal match inside of a [RegExp].
///
/// The special characters are: \ ^ $ . | + [ ] ( ) { }
/// as defined here: http://ecma-international.org/ecma-262/5.1/#sec-15.10
String escapeRegex(String str) => str.splitMapJoin(_specialChars,
onMatch: (Match m) => '\\${m.group(0)}', onNonMatch: (s) => s);
/// Returns a [Pattern] that matches against every pattern in [include] and
/// returns all the matches. If the input string matches against any pattern in
/// [exclude] no matches are returned.
Pattern matchAny(Iterable<Pattern> include, {Iterable<Pattern>? exclude}) =>
_MultiPattern(include, exclude: exclude);
class _MultiPattern extends Pattern {
_MultiPattern(this.include, {this.exclude});
final Iterable<Pattern> include;
final Iterable<Pattern>? exclude;
@override
Iterable<Match> allMatches(String str, [int start = 0]) {
final allMatches = <Match>[];
for (final pattern in include) {
var matches = pattern.allMatches(str, start);
if (_hasMatch(matches)) {
if (exclude != null) {
for (final excludePattern in exclude!) {
if (_hasMatch(excludePattern.allMatches(str, start))) {
return [];
}
}
}
allMatches.addAll(matches);
}
}
return allMatches;
}
@override
Match? matchAsPrefix(String str, [int start = 0]) {
for (final match in allMatches(str)) {
if (match.start == start) {
return match;
}
}
return null;
}
}
/// Returns true if [pattern] has a single match in [str] that matches the
/// whole string, not a substring.
bool matchesFull(Pattern pattern, String str) {
var match = pattern.matchAsPrefix(str);
return match != null && match.end == str.length;
}
bool _hasMatch(Iterable<Match> matches) => matches.iterator.moveNext();
// TODO(justin): add more detailed documentation and explain how matching
// differs or is similar to globs in Python and various shells.
/// A [Pattern] that matches against filesystem path-like strings with
/// wildcards.
///
/// The pattern matches strings as follows:
/// * The whole string must match, not a substring
/// * Any non wildcard is matched as a literal
/// * '*' matches one or more characters except '/'
/// * '?' matches exactly one character except '/'
/// * '**' matches one or more characters including '/'
class Glob implements Pattern {
Glob(this.pattern) : regex = _regexpFromGlobPattern(pattern);
final RegExp regex;
final String pattern;
@override
Iterable<Match> allMatches(String str, [int start = 0]) =>
regex.allMatches(str, start);
@override
Match? matchAsPrefix(String string, [int start = 0]) =>
regex.matchAsPrefix(string, start);
bool hasMatch(String str) => regex.hasMatch(str);
@override
String toString() => pattern;
@override
int get hashCode => pattern.hashCode;
@override
bool operator ==(Object other) => other is Glob && pattern == other.pattern;
}
RegExp _regexpFromGlobPattern(String pattern) {
var sb = StringBuffer();
sb.write('^');
var chars = pattern.split('');
for (var i = 0; i < chars.length; i++) {
var c = chars[i];
if (_specialChars.hasMatch(c)) {
sb.write('\\$c');
} else if (c == '*') {
if ((i + 1 < chars.length) && (chars[i + 1] == '*')) {
sb.write('.*');
i++;
} else {
sb.write('[^/]*');
}
} else if (c == '?') {
sb.write('[^/]');
} else {
sb.write(c);
}
}
sb.write(r'$');
return RegExp(sb.toString());
}