blob: 92066850dd8b2a6c0a45144c0e7a02f50aa97c19 [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of dart._js_helper;
// Helper method used by internal libraries.
regExpGetNative(JSSyntaxRegExp regexp) => regexp._nativeRegExp;
/**
* Returns a native version of the RegExp with the global flag set.
*
* The RegExp's `lastIndex` property is zero when it is returned.
*
* The returned regexp is shared, and its `lastIndex` property may be
* modified by other uses, so the returned regexp must be used immediately
* when it's returned, with no user-provided code run in between.
*/
regExpGetGlobalNative(JSSyntaxRegExp regexp) {
var nativeRegexp = regexp._nativeGlobalVersion;
JS("void", "#.lastIndex = 0", nativeRegexp);
return nativeRegexp;
}
/**
* Computes the number of captures in a regexp.
*
* This currently involves creating a new RegExp object with a different
* source and running it against the empty string (the last part is usually
* fast).
*
* The JSSyntaxRegExp could cache the result, and set the cache any time
* it finds a match.
*/
int regExpCaptureCount(JSSyntaxRegExp regexp) {
var nativeAnchoredRegExp = regexp._nativeAnchoredVersion;
JSExtendableArray match =
JS('JSExtendableArray', "#.exec('')", nativeAnchoredRegExp);
// The native-anchored regexp always have one capture more than the original,
// and always matches the empty string.
return match.length - 2;
}
class JSSyntaxRegExp implements RegExp {
final String pattern;
final _nativeRegExp;
var _nativeGlobalRegExp;
var _nativeAnchoredRegExp;
String toString() => "RegExp/$pattern/";
JSSyntaxRegExp(String source,
{bool multiLine = false, bool caseSensitive = true})
: this.pattern = source,
this._nativeRegExp =
makeNative(source, multiLine, caseSensitive, false);
get _nativeGlobalVersion {
if (_nativeGlobalRegExp != null) return _nativeGlobalRegExp;
return _nativeGlobalRegExp =
makeNative(pattern, _isMultiLine, _isCaseSensitive, true);
}
get _nativeAnchoredVersion {
if (_nativeAnchoredRegExp != null) return _nativeAnchoredRegExp;
// An "anchored version" of a regexp is created by adding "|()" to the
// source. This means that the regexp always matches at the first position
// that it tries, and you can see if the original regexp matched, or it
// was the added zero-width match that matched, by looking at the last
// capture. If it is a String, the match participated, otherwise it didn't.
return _nativeAnchoredRegExp =
makeNative("$pattern|()", _isMultiLine, _isCaseSensitive, true);
}
bool get _isMultiLine => JS("bool", "#.multiline", _nativeRegExp);
bool get _isCaseSensitive => JS("bool", "!#.ignoreCase", _nativeRegExp);
static makeNative(@nullCheck String source, bool multiLine,
bool caseSensitive, bool global) {
String m = multiLine ? 'm' : '';
String i = caseSensitive ? '' : 'i';
String g = global ? 'g' : '';
// We're using the JavaScript's try catch instead of the Dart one
// to avoid dragging in Dart runtime support just because of using
// RegExp.
var regexp = JS(
'',
'(function() {'
'try {'
'return new RegExp(#, # + # + #);'
'} catch (e) {'
'return e;'
'}'
'})()',
source,
m,
i,
g);
if (JS('bool', '# instanceof RegExp', regexp)) return regexp;
// The returned value is the JavaScript exception. Turn it into a
// Dart exception.
String errorMessage = JS('String', r'String(#)', regexp);
throw FormatException("Illegal RegExp pattern: $source, $errorMessage");
}
Match firstMatch(@nullCheck String string) {
List m = JS('JSExtendableArray|Null', r'#.exec(#)', _nativeRegExp, string);
if (m == null) return null;
return _MatchImplementation(this, JSArray<String>.of(m));
}
@notNull
bool hasMatch(@nullCheck String string) {
return JS('bool', r'#.test(#)', _nativeRegExp, string);
}
String stringMatch(String string) {
var match = firstMatch(string);
if (match != null) return match.group(0);
return null;
}
Iterable<Match> allMatches(@nullCheck String string,
[@nullCheck int start = 0]) {
if (start < 0 || start > string.length) {
throw RangeError.range(start, 0, string.length);
}
return _AllMatchesIterable(this, string, start);
}
Match _execGlobal(String string, int start) {
Object regexp = _nativeGlobalVersion;
JS("void", "#.lastIndex = #", regexp, start);
List match = JS("JSExtendableArray|Null", "#.exec(#)", regexp, string);
if (match == null) return null;
return _MatchImplementation(this, JSArray<String>.of(match));
}
Match _execAnchored(String string, int start) {
Object regexp = _nativeAnchoredVersion;
JS("void", "#.lastIndex = #", regexp, start);
List match = JS("JSExtendableArray|Null", "#.exec(#)", regexp, string);
if (match == null) return null;
// If the last capture group participated, the original regexp did not
// match at the start position.
if (match[match.length - 1] != null) return null;
match.length -= 1;
return _MatchImplementation(this, JSArray<String>.of(match));
}
Match matchAsPrefix(String string, [int start = 0]) {
if (start < 0 || start > string.length) {
throw RangeError.range(start, 0, string.length);
}
return _execAnchored(string, start);
}
bool get isMultiLine => _isMultiLine;
bool get isCaseSensitive => _isCaseSensitive;
}
class _MatchImplementation implements RegExpMatch {
final Pattern pattern;
// Contains a JS RegExp match object.
// It is an Array of String values with extra "index" and "input" properties.
final List<String> _match;
_MatchImplementation(this.pattern, this._match) {
assert(JS("var", "#.input", _match) is String);
assert(JS("var", "#.index", _match) is int);
}
String get input => JS("String", "#.input", _match);
int get start => JS("int", "#.index", _match);
int get end => start + _match[0].length;
String group(int index) => _match[index];
String operator [](int index) => group(index);
int get groupCount => _match.length - 1;
List<String> groups(List<int> groups) {
List<String> out = [];
for (int i in groups) {
out.add(group(i));
}
return out;
}
String namedGroup(String name) {
var groups = JS('Object', '#.groups', _match);
if (groups != null) {
var result = JS('String|Null', '#[#]', groups, name);
if (result != null || JS('bool', '# in #', name, groups)) {
return result;
}
}
throw ArgumentError.value(name, "name", "Not a capture group name");
}
Iterable<String> get groupNames {
var groups = JS('Object', '#.groups', _match);
if (groups != null) {
var keys = JSArray<String>.of(JS('', 'Object.keys(#)', groups));
return SubListIterable(keys, 0, null);
}
return Iterable.empty();
}
}
class _AllMatchesIterable extends IterableBase<Match> {
final JSSyntaxRegExp _re;
final String _string;
final int _start;
_AllMatchesIterable(this._re, this._string, this._start);
Iterator<Match> get iterator => _AllMatchesIterator(_re, _string, _start);
}
class _AllMatchesIterator implements Iterator<Match> {
final JSSyntaxRegExp _regExp;
String _string;
int _nextIndex;
Match _current;
_AllMatchesIterator(this._regExp, this._string, this._nextIndex);
Match get current => _current;
bool moveNext() {
if (_string == null) return false;
if (_nextIndex <= _string.length) {
var match = _regExp._execGlobal(_string, _nextIndex);
if (match != null) {
_current = match;
int nextIndex = match.end;
if (match.start == nextIndex) {
nextIndex++;
}
_nextIndex = nextIndex;
return true;
}
}
_current = null;
_string = null; // Marks iteration as ended.
return false;
}
}
/** Find the first match of [regExp] in [string] at or after [start]. */
Match firstMatchAfter(JSSyntaxRegExp regExp, String string, int start) {
return regExp._execGlobal(string, start);
}