blob: 91c2bae6f5e088648f7569f2c1606cd916646b88 [file] [log] [blame]
// Copyright (c) 2021, 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.
// Utilities for converting between JavaScript source-code Strings and the
// String value they represent.
import 'characters.dart' as charCodes;
class StringToSourceKind {
/// [true] if preferable to use double quotes, [false] if preferable to use
/// single quotes.
final bool doubleQuotes;
/// [true] if contents require no escaping with the preferred quoting.
final bool simple;
const StringToSourceKind({this.doubleQuotes, this.simple});
String get quote => doubleQuotes ? '"' : "'";
}
class StringToSource {
const StringToSource();
static StringToSourceKind analyze(String value, {/*required*/ bool utf8}) {
final ascii = !utf8;
int singleQuotes = 0;
int doubleQuotes = 0;
int otherEscapes = 0;
int unpairedSurrogates = 0;
for (int rune in value.runes) {
if (rune == charCodes.$BACKSLASH) {
++otherEscapes;
} else if (rune == charCodes.$SQ) {
++singleQuotes;
} else if (rune == charCodes.$DQ) {
++doubleQuotes;
} else if (rune == charCodes.$LF ||
rune == charCodes.$CR ||
rune == charCodes.$LS ||
rune == charCodes.$PS) {
// Line terminators.
++otherEscapes;
} else if (rune == charCodes.$BS ||
rune == charCodes.$TAB ||
rune == charCodes.$VTAB ||
rune == charCodes.$FF) {
++otherEscapes;
} else if (ascii && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
++otherEscapes;
} else if (_isUnpairedSurrogate(rune)) {
// Need to escape unpaired surrogates in a UTF8-encoded output otherwise
// the output would be malformed.
++unpairedSurrogates;
}
}
if (otherEscapes == 0 && unpairedSurrogates == 0) {
if (doubleQuotes == 0) {
return const StringToSourceKind(doubleQuotes: true, simple: true);
}
if (singleQuotes == 0) {
return const StringToSourceKind(doubleQuotes: false, simple: true);
}
}
return doubleQuotes <= singleQuotes
? const StringToSourceKind(doubleQuotes: true, simple: false)
: const StringToSourceKind(doubleQuotes: false, simple: false);
}
static void writeString(
StringBuffer sb, String string, StringToSourceKind kind,
{/*required*/ bool utf8}) {
for (int rune in string.runes) {
String escape = _irregularEscape(rune, kind.doubleQuotes);
if (escape != null) {
sb.write(escape);
continue;
}
if (rune == charCodes.$LS ||
rune == charCodes.$PS ||
_isUnpairedSurrogate(rune) ||
!utf8 && (rune < charCodes.$SPACE || rune >= charCodes.$DEL)) {
if (rune < 0x100) {
sb.write(r'\x');
sb.write(rune.toRadixString(16).padLeft(2, '0'));
} else if (rune < 0x10000) {
sb.write(r'\u');
sb.write(rune.toRadixString(16).padLeft(4, '0'));
} else {
// Not all browsers accept the ES6 \u{zzzzzz} encoding, so emit two
// surrogate pairs.
var bits = rune - 0x10000;
var leading = 0xD800 | (bits >> 10);
var trailing = 0xDC00 | (bits & 0x3ff);
sb.write(r'\u');
sb.write(leading.toRadixString(16));
sb.write(r'\u');
sb.write(trailing.toRadixString(16));
}
} else {
sb.writeCharCode(rune);
}
}
}
static bool _isUnpairedSurrogate(int code) => (code & 0xFFFFF800) == 0xD800;
static String _irregularEscape(int code, bool useDoubleQuotes) {
switch (code) {
case charCodes.$SQ:
return useDoubleQuotes ? r"'" : r"\'";
case charCodes.$DQ:
return useDoubleQuotes ? r'\"' : r'"';
case charCodes.$BACKSLASH:
return r'\\';
case charCodes.$BS:
return r'\b';
case charCodes.$TAB:
return r'\t';
case charCodes.$LF:
return r'\n';
case charCodes.$VTAB:
return r'\v';
case charCodes.$FF:
return r'\f';
case charCodes.$CR:
return r'\r';
}
return null;
}
}