NativeString.java
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript;
import static org.mozilla.javascript.ScriptRuntime.rangeError;
import static org.mozilla.javascript.ScriptRuntimeES6.requireObjectCoercible;
import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex;
/**
* This class implements the String native object.
*
* <p>See ECMA 15.5.
*
* <p>String methods for dealing with regular expressions are ported directly from C. Latest port is
* from version 1.40.12.19 in the JSFUN13_BRANCH.
*
* @author Mike McCabe
* @author Norris Boyd
* @author Ronald Brill
*/
final class NativeString extends ScriptableObject {
private static final long serialVersionUID = 920268368584188687L;
private static final String CLASS_NAME = "String";
private final CharSequence string;
static void init(Scriptable scope, boolean sealed) {
LambdaConstructor c =
new LambdaConstructor(
scope,
CLASS_NAME,
1,
NativeString::js_constructorFunc,
NativeString::js_constructor);
c.setPrototypePropertyAttributes(DONTENUM | READONLY | PERMANENT);
c.setPrototypeScriptable(new NativeString(""));
defConsMethod(c, scope, "fromCharCode", 1, NativeString::js_fromCharCode);
defConsMethod(c, scope, "fromCodePoint", 1, NativeString::js_fromCodePoint);
defConsMethod(c, scope, "raw", 1, NativeString::js_raw);
/*
* All of the methods below are on the constructor for compatibility with ancient Rhino
* versions. They are no longer part of ECMAScript. The "wrapConstructor" method is a
* technique used in the past in Rhino to adapt the instance functions so that they
* may be called on the constructor directly.
*/
defConsMethod(c, scope, "charAt", 1, wrapConstructor(NativeString::js_charAt));
defConsMethod(c, scope, "charCodeAt", 1, wrapConstructor(NativeString::js_charCodeAt));
defConsMethod(c, scope, "indexOf", 2, wrapConstructor(NativeString::js_indexOf));
defConsMethod(c, scope, "lastIndexOf", 2, wrapConstructor(NativeString::js_lastIndexOf));
defConsMethod(c, scope, "split", 3, wrapConstructor(NativeString::js_split));
defConsMethod(c, scope, "substring", 3, wrapConstructor(NativeString::js_substring));
defConsMethod(c, scope, "toLowerCase", 1, wrapConstructor(NativeString::js_toLowerCase));
defConsMethod(c, scope, "toUpperCase", 1, wrapConstructor(NativeString::js_toUpperCase));
defConsMethod(c, scope, "substr", 3, wrapConstructor(NativeString::js_substr));
defConsMethod(c, scope, "concat", 2, wrapConstructor(NativeString::js_concat));
defConsMethod(c, scope, "slice", 3, wrapConstructor(NativeString::js_slice));
defConsMethod(
c,
scope,
"equalsIgnoreCase",
2,
wrapConstructor(NativeString::js_equalsIgnoreCase));
defConsMethod(c, scope, "match", 2, wrapConstructor(NativeString::js_match));
defConsMethod(c, scope, "search", 2, wrapConstructor(NativeString::js_search));
defConsMethod(c, scope, "replace", 2, wrapConstructor(NativeString::js_replace));
defConsMethod(c, scope, "replaceAll", 2, wrapConstructor(NativeString::js_replaceAll));
defConsMethod(
c, scope, "localeCompare", 2, wrapConstructor(NativeString::js_localeCompare));
defConsMethod(
c,
scope,
"toLocaleLowerCase",
1,
wrapConstructor(NativeString::js_toLocaleLowerCase));
/* Back to prototype methods -- these are all part of ECMAScript */
defProtoMethod(c, scope, SymbolKey.ITERATOR, 0, NativeString::js_iterator);
defProtoMethod(c, scope, "toString", 0, NativeString::js_toString);
defProtoMethod(c, scope, "toSource", 0, NativeString::js_toSource);
defProtoMethod(c, scope, "valueOf", 0, NativeString::js_toString);
defProtoMethodWithoutProto(c, scope, "charAt", 1, NativeString::js_charAt);
defProtoMethodWithoutProto(c, scope, "charCodeAt", 1, NativeString::js_charCodeAt);
defProtoMethodWithoutProto(c, scope, "indexOf", 1, NativeString::js_indexOf);
defProtoMethodWithoutProto(c, scope, "lastIndexOf", 1, NativeString::js_lastIndexOf);
defProtoMethodWithoutProto(c, scope, "split", 2, NativeString::js_split);
defProtoMethodWithoutProto(c, scope, "substring", 2, NativeString::js_substring);
defProtoMethodWithoutProto(c, scope, "toLowerCase", 0, NativeString::js_toLowerCase);
defProtoMethodWithoutProto(c, scope, "toUpperCase", 0, NativeString::js_toUpperCase);
defProtoMethodWithoutProto(c, scope, "substr", 2, NativeString::js_substr);
defProtoMethodWithoutProto(c, scope, "concat", 1, NativeString::js_concat);
defProtoMethodWithoutProto(c, scope, "slice", 2, NativeString::js_slice);
defProtoMethod(c, scope, "bold", 0, NativeString::js_bold);
defProtoMethod(c, scope, "italics", 0, NativeString::js_italics);
defProtoMethod(c, scope, "fixed", 0, NativeString::js_fixed);
defProtoMethod(c, scope, "strike", 0, NativeString::js_strike);
defProtoMethod(c, scope, "small", 0, NativeString::js_small);
defProtoMethod(c, scope, "big", 0, NativeString::js_big);
defProtoMethod(c, scope, "blink", 0, NativeString::js_blink);
defProtoMethod(c, scope, "sup", 0, NativeString::js_sup);
defProtoMethod(c, scope, "sub", 0, NativeString::js_sub);
defProtoMethod(c, scope, "fontsize", 0, NativeString::js_fontsize);
defProtoMethod(c, scope, "fontcolor", 0, NativeString::js_fontcolor);
defProtoMethod(c, scope, "link", 0, NativeString::js_link);
defProtoMethod(c, scope, "anchor", 0, NativeString::js_anchor);
defProtoMethod(c, scope, "equals", 1, NativeString::js_equals);
defProtoMethod(c, scope, "equalsIgnoreCase", 1, NativeString::js_equalsIgnoreCase);
defProtoMethodWithoutProto(c, scope, "match", 1, NativeString::js_match);
defProtoMethodWithoutProto(c, scope, "matchAll", 1, NativeString::js_matchAll);
defProtoMethodWithoutProto(c, scope, "search", 1, NativeString::js_search);
defProtoMethodWithoutProto(c, scope, "replace", 2, NativeString::js_replace);
defProtoMethodWithoutProto(c, scope, "replaceAll", 2, NativeString::js_replaceAll);
defProtoMethod(c, scope, "at", 1, NativeString::js_at);
defProtoMethodWithoutProto(c, scope, "localeCompare", 1, NativeString::js_localeCompare);
defProtoMethodWithoutProto(
c, scope, "toLocaleLowerCase", 0, NativeString::js_toLocaleLowerCase);
defProtoMethodWithoutProto(
c, scope, "toLocaleUpperCase", 0, NativeString::js_toLocaleUpperCase);
defProtoMethod(c, scope, "trim", 0, NativeString::js_trim);
defProtoMethod(c, scope, "trimLeft", 0, NativeString::js_trimLeft);
defProtoMethod(c, scope, "trimStart", 0, NativeString::js_trimLeft);
defProtoMethod(c, scope, "trimRight", 0, NativeString::js_trimRight);
defProtoMethod(c, scope, "trimEnd", 0, NativeString::js_trimRight);
defProtoMethod(c, scope, "includes", 1, NativeString::js_includes);
defProtoMethod(c, scope, "startsWith", 1, NativeString::js_startsWith);
defProtoMethod(c, scope, "endsWith", 1, NativeString::js_endsWith);
defProtoMethod(c, scope, "normalize", 0, NativeString::js_normalize);
defProtoMethod(c, scope, "repeat", 1, NativeString::js_repeat);
defProtoMethod(c, scope, "codePointAt", 1, NativeString::js_codePointAt);
defProtoMethod(c, scope, "padStart", 1, NativeString::js_padStart);
defProtoMethod(c, scope, "padEnd", 1, NativeString::js_padEnd);
defProtoMethod(c, scope, "isWellFormed", 0, NativeString::js_isWellFormed);
defProtoMethod(c, scope, "toWellFormed", 0, NativeString::js_toWellFormed);
if (sealed) {
c.sealObject();
((NativeString) c.getPrototypeProperty()).sealObject();
}
ScriptableObject.defineProperty(scope, CLASS_NAME, c, DONTENUM);
}
private static void defConsMethod(
LambdaConstructor c,
Scriptable scope,
String name,
int length,
SerializableCallable target) {
c.defineConstructorMethod(scope, name, length, target, DONTENUM);
}
private static void defProtoMethod(
LambdaConstructor c,
Scriptable scope,
String name,
int length,
SerializableCallable target) {
c.definePrototypeMethod(scope, name, length, target, DONTENUM, DONTENUM | READONLY, true);
}
private static void defProtoMethod(
LambdaConstructor c,
Scriptable scope,
SymbolKey key,
int length,
SerializableCallable target) {
c.definePrototypeMethod(scope, key, length, target, DONTENUM, DONTENUM | READONLY);
}
private static void defProtoMethodWithoutProto(
LambdaConstructor c,
Scriptable scope,
String name,
int length,
SerializableCallable target) {
c.definePrototypeMethod(scope, name, length, target, DONTENUM, DONTENUM | READONLY, false);
}
NativeString(CharSequence s) {
string = s;
// This needs to happen right here because ScriptRuntime sometimes
// constructs strings directly without using the JS constructor.
defineProperty("length", s::length, null, DONTENUM | READONLY | PERMANENT);
}
@Override
public String getClassName() {
return CLASS_NAME;
}
private static SerializableCallable wrapConstructor(SerializableCallable target) {
return (Context cx, Scriptable scope, Scriptable origThis, Object[] origArgs) -> {
Scriptable thisObj;
Object[] newArgs;
if (origArgs.length > 0) {
thisObj =
ScriptRuntime.toObject(
cx, scope, ScriptRuntime.toCharSequence(origArgs[0]));
newArgs = new Object[origArgs.length - 1];
System.arraycopy(origArgs, 1, newArgs, 0, newArgs.length);
} else {
thisObj = ScriptRuntime.toObject(cx, scope, ScriptRuntime.toCharSequence(origThis));
newArgs = origArgs;
}
return target.call(cx, scope, thisObj, newArgs);
};
}
private static Scriptable js_constructor(Context cx, Scriptable scope, Object[] args) {
CharSequence s;
if (args.length == 0) {
s = "";
} else {
s = ScriptRuntime.toCharSequence(args[0]);
}
return new NativeString(s);
}
private static Object js_constructorFunc(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence s;
if (args.length == 0) {
s = "";
} else if (ScriptRuntime.isSymbol(args[0])) {
// 19.4.3.2 et.al. Convert a symbol to a string with String() but not
// new String()
s = args[0].toString();
} else {
s = ScriptRuntime.toCharSequence(args[0]);
}
// String(val) converts val to a string value.
return s instanceof String ? s : s.toString();
}
private static Object js_fromCharCode(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
int n = args.length;
if (n < 1) {
return "";
}
char[] chars = new char[n];
for (int i = 0; i != n; ++i) {
chars[i] = ScriptRuntime.toUint16(args[i]);
}
return new String(chars);
}
private static Object js_fromCodePoint(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
int n = args.length;
if (n < 1) {
return "";
}
int[] codePoints = new int[n];
for (int i = 0; i != n; i++) {
Object arg = args[i];
int codePoint = ScriptRuntime.toInt32(arg);
double num = ScriptRuntime.toNumber(arg);
if (!ScriptRuntime.eqNumber(num, codePoint) || !Character.isValidCodePoint(codePoint)) {
throw rangeError("Invalid code point " + ScriptRuntime.toString(arg));
}
codePoints[i] = codePoint;
}
return new String(codePoints, 0, n);
}
private static Object js_charAt(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return charAt(cx, thisObj, args, false);
}
private static Object js_charCodeAt(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return charAt(cx, thisObj, args, true);
}
private static Object charAt(Context cx, Scriptable thisObj, Object[] args, boolean getCode) {
// See ECMA 15.5.4.[4,5]
CharSequence target =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "charAt"));
double pos = ScriptRuntime.toInteger(args, 0);
if (pos < 0 || pos >= target.length()) {
if (!getCode) return "";
return ScriptRuntime.NaNobj;
}
char c = target.charAt((int) pos);
if (!getCode) return String.valueOf(c);
return ScriptRuntime.wrapInt(c);
}
private static Object js_indexOf(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "indexOf"));
String searchStr = ScriptRuntime.toString(args, 0);
double position = ScriptRuntime.toInteger(args, 1);
if (searchStr.isEmpty()) {
return position > target.length() ? target.length() : (int) position;
}
if (position > target.length()) {
return -1;
}
if (position < 0) position = 0;
return target.indexOf(searchStr, (int) position);
}
private static Object js_startsWith(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "startsWith"));
checkValidRegex(cx, args, 0, "startsWith");
String searchStr = ScriptRuntime.toString(args, 0);
double position = ScriptRuntime.toInteger(args, 1);
if (position < 0) position = 0;
else if (position > target.length()) position = target.length();
return target.startsWith(searchStr, (int) position);
}
private static Object js_endsWith(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "endsWith"));
checkValidRegex(cx, args, 0, "endsWith");
String searchStr = ScriptRuntime.toString(args, 0);
double position = ScriptRuntime.toInteger(args, 1);
if (position < 0) position = 0;
else if (Double.isNaN(position) || position > target.length()) position = target.length();
if (args.length == 0
|| args.length == 1
|| (args.length == 2 && Undefined.isUndefined(args[1]))) position = target.length();
return target.substring(0, (int) position).endsWith(searchStr);
}
private static Object js_includes(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "includes"));
String searchStr = ScriptRuntime.toString(args, 0);
checkValidRegex(cx, args, 0, "includes");
int position = (int) ScriptRuntime.toInteger(args, 1);
return target.indexOf(searchStr, position) != -1;
}
private static void checkValidRegex(Context cx, Object[] args, int pos, String functionName) {
if (args.length > pos && args[pos] instanceof Scriptable) {
RegExpProxy reProxy = ScriptRuntime.getRegExpProxy(cx);
if (reProxy != null) {
Scriptable arg = (Scriptable) args[pos];
if (reProxy.isRegExp(arg)) {
if (ScriptableObject.isTrue(
ScriptableObject.getProperty(arg, SymbolKey.MATCH))) {
throw ScriptRuntime.typeErrorById(
"msg.first.arg.not.regexp", CLASS_NAME, functionName);
}
}
}
}
}
private static Object js_split(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMAScript spec 22.1.3.23
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "split");
if (cx.getLanguageVersion() <= Context.VERSION_1_8) {
// Use old algorithm for backward compatibility
return ScriptRuntime.checkRegExpProxy(cx)
.js_split(cx, scope, ScriptRuntime.toString(o), args);
}
Object separator = args.length > 0 ? args[0] : Undefined.instance;
Object limit = args.length > 1 ? args[1] : Undefined.instance;
if (!Undefined.isUndefined(separator) && separator != null) {
Object splitter = ScriptRuntime.getObjectElem(separator, SymbolKey.SPLIT, cx, scope);
// If method is not undefined, it should be a Callable
if (splitter != null && !Undefined.isUndefined(splitter)) {
if (!(splitter instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
separator, splitter, SymbolKey.SPLIT.getName());
}
return ((Callable) splitter)
.call(
cx,
scope,
ScriptRuntime.toObject(scope, separator),
new Object[] {
o instanceof NativeString ? ((NativeString) o).string : o,
limit,
});
}
}
String s = ScriptRuntime.toString(o);
long lim;
if (Undefined.isUndefined(limit)) {
lim = Integer.MAX_VALUE;
} else {
lim = ScriptRuntime.toUint32(limit);
}
String r = ScriptRuntime.toString(separator);
if (lim == 0) {
return cx.newArray(scope, 0);
}
if (Undefined.isUndefined(separator)) {
return cx.newArray(scope, new Object[] {s});
}
int separatorLength = r.length();
if (separatorLength == 0) {
int strLen = s.length();
int outLen = ScriptRuntime.clamp((int) lim, 0, strLen);
String head = s.substring(0, outLen);
List<Object> codeUnits = new ArrayList<>();
for (int i = 0; i < head.length(); ) {
char c = head.charAt(i);
codeUnits.add(Character.toString(c));
i += Character.charCount(c);
}
return cx.newArray(scope, codeUnits.toArray());
}
if (s.isEmpty()) {
return cx.newArray(scope, new Object[] {s});
}
List<String> substrings = new ArrayList<>();
int i = 0;
int j = s.indexOf(r);
while (j != -1) {
String t = s.substring(i, j);
substrings.add(t);
if (substrings.size() >= lim) {
return cx.newArray(scope, substrings.toArray());
}
i = j + separatorLength;
j = s.indexOf(r, i);
}
String t = s.substring(i);
substrings.add(t);
return cx.newArray(scope, substrings.toArray());
}
private static NativeString realThis(Scriptable thisObj) {
return LambdaConstructor.convertThisObject(thisObj, NativeString.class);
}
private static Object js_iterator(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return new NativeStringIterator(
scope, requireObjectCoercible(cx, thisObj, CLASS_NAME, "[Symbol.iterator]"));
}
private static Object js_toString(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// ECMA 15.5.4.2: the toString function is not generic.
CharSequence cs = realThis(thisObj).string;
return cs instanceof String ? cs : cs.toString();
}
private static Object js_toSource(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence s = realThis(thisObj).string;
return "(new String(\"" + ScriptRuntime.escapeString(s.toString()) + "\"))";
}
/*
* HTML composition aids.
*/
private static String tagify(
Context cx,
Scriptable thisObj,
String functionName,
String tag,
String attribute,
Object[] args) {
String str =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, functionName));
StringBuilder result = new StringBuilder();
result.append('<').append(tag);
if (attribute != null && !attribute.isEmpty()) {
String attributeValue = ScriptRuntime.toString(args, 0);
attributeValue = attributeValue.replace("\"", """);
result.append(' ').append(attribute).append("=\"").append(attributeValue).append('"');
}
result.append('>').append(str).append("</").append(tag).append('>');
return result.toString();
}
public CharSequence toCharSequence() {
return string;
}
@Override
public String toString() {
return string instanceof String ? (String) string : string.toString();
}
/* Make array-style property lookup work for strings.
* XXX is this ECMA? A version check is probably needed. In js too.
*/
@Override
public Object get(int index, Scriptable start) {
if (0 <= index && index < string.length()) {
return String.valueOf(string.charAt(index));
}
return super.get(index, start);
}
@Override
public void put(int index, Scriptable start, Object value) {
if (0 <= index && index < string.length()) {
return;
}
super.put(index, start, value);
}
@Override
public boolean has(int index, Scriptable start) {
if (0 <= index && index < string.length()) {
return true;
}
return super.has(index, start);
}
@Override
public int getAttributes(int index) {
if (0 <= index && index < string.length()) {
int attribs = READONLY | PERMANENT;
if (Context.getContext().getLanguageVersion() < Context.VERSION_ES6) {
attribs |= DONTENUM;
}
return attribs;
}
return super.getAttributes(index);
}
@Override
protected Object[] getIds(boolean nonEnumerable, boolean getSymbols) {
// In ES6, Strings have entries in the property map for each character.
Context cx = Context.getCurrentContext();
if ((cx != null) && (cx.getLanguageVersion() >= Context.VERSION_ES6)) {
Object[] sids = super.getIds(nonEnumerable, getSymbols);
Object[] a = new Object[sids.length + string.length()];
int i;
for (i = 0; i < string.length(); i++) {
a[i] = i;
}
System.arraycopy(sids, 0, a, i, sids.length);
return a;
}
return super.getIds(nonEnumerable, getSymbols);
}
@Override
protected ScriptableObject getOwnPropertyDescriptor(Context cx, Object id) {
if (!(id instanceof Symbol)
&& (cx != null)
&& (cx.getLanguageVersion() >= Context.VERSION_ES6)) {
StringIdOrIndex s = ScriptRuntime.toStringIdOrIndex(id);
if (s.stringId == null && 0 <= s.index && s.index < string.length()) {
String value = String.valueOf(string.charAt(s.index));
return defaultIndexPropertyDescriptor(value);
}
}
return super.getOwnPropertyDescriptor(cx, id);
}
private ScriptableObject defaultIndexPropertyDescriptor(Object value) {
Scriptable scope = getParentScope();
if (scope == null) scope = this;
ScriptableObject desc = new NativeObject();
ScriptRuntime.setBuiltinProtoAndParent(desc, scope, TopLevel.Builtins.Object);
desc.defineProperty("value", value, EMPTY);
desc.defineProperty("writable", Boolean.FALSE, EMPTY);
desc.defineProperty("enumerable", Boolean.TRUE, EMPTY);
desc.defineProperty("configurable", Boolean.FALSE, EMPTY);
return desc;
}
/*
*
* See ECMA 22.1.3.13
*
*/
private static Object js_match(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "match");
Object regexp = args.length > 0 ? args[0] : Undefined.instance;
RegExpProxy regExpProxy = ScriptRuntime.checkRegExpProxy(cx);
if (regexp != null && !Undefined.isUndefined(regexp)) {
Object matcher = ScriptRuntime.getObjectElem(regexp, SymbolKey.MATCH, cx, scope);
// If method is not undefined, it should be a Callable
if (matcher != null && !Undefined.isUndefined(matcher)) {
if (!(matcher instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
regexp, matcher, SymbolKey.MATCH.getName());
}
return ((Callable) matcher)
.call(cx, scope, ScriptRuntime.toObject(scope, regexp), new Object[] {o});
}
}
String s = ScriptRuntime.toString(o);
String regexpToString = Undefined.isUndefined(regexp) ? "" : ScriptRuntime.toString(regexp);
String flags = null;
// Not standard; Done for backward compatibility
if (cx.getLanguageVersion() < Context.VERSION_1_6 && args.length > 1) {
flags = ScriptRuntime.toString(args[1]);
}
Object compiledRegExp = regExpProxy.compileRegExp(cx, regexpToString, flags);
Scriptable rx = regExpProxy.wrapRegExp(cx, scope, compiledRegExp);
Object method = ScriptRuntime.getObjectElem(rx, SymbolKey.MATCH, cx, scope);
if (!(method instanceof Callable)) {
throw ScriptRuntime.notFunctionError(rx, method, SymbolKey.MATCH.getName());
}
return ((Callable) method).call(cx, scope, rx, new Object[] {s});
}
/*
*
* See ECMA 15.5.4.7
*
*/
private static Object js_lastIndexOf(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "lastIndexOf"));
String search = ScriptRuntime.toString(args, 0);
double end = ScriptRuntime.toNumber(args, 1);
if (Double.isNaN(end) || end > target.length()) end = target.length();
else if (end < 0) end = 0;
return target.lastIndexOf(search, (int) end);
}
/*
* See ECMA 15.5.4.15
*/
private static Object js_substring(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence target =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "substring"));
int length = target.length();
double start = ScriptRuntime.toInteger(args, 0);
double end;
if (start < 0) start = 0;
else if (start > length) start = length;
if (args.length <= 1 || args[1] == Undefined.instance) {
end = length;
} else {
end = ScriptRuntime.toInteger(args[1]);
if (end < 0) end = 0;
else if (end > length) end = length;
// swap if end < start
if (end < start) {
if (cx.getLanguageVersion() != Context.VERSION_1_2) {
double temp = start;
start = end;
end = temp;
} else {
// Emulate old JDK1.0 java.lang.String.substring()
end = start;
}
}
}
return target.subSequence((int) start, (int) end);
}
private static Object js_toLowerCase(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMA 15.5.4.11
String thisStr =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "toLowerCase"));
return thisStr.toLowerCase(Locale.ROOT);
}
private static Object js_toUpperCase(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMA 15.5.4.12
String thisStr =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "toUpperCase"));
return thisStr.toUpperCase(Locale.ROOT);
}
int getLength() {
return string.length();
}
/*
* Non-ECMA methods.
*/
private static CharSequence js_substr(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence target =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "substr"));
if (args.length < 1) {
return target;
}
double begin = ScriptRuntime.toInteger(args[0]);
double end;
int length = target.length();
if (begin < 0) {
begin += length;
if (begin < 0) begin = 0;
} else if (begin > length) {
begin = length;
}
end = length;
if (args.length > 1) {
Object lengthArg = args[1];
if (!Undefined.isUndefined(lengthArg)) {
end = ScriptRuntime.toInteger(lengthArg);
if (end < 0) {
end = 0;
}
end += begin;
if (end > length) {
end = length;
}
}
}
return target.subSequence((int) begin, (int) end);
}
/*
* Python-esque sequence operations.
*/
private static String js_concat(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String target =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "concat"));
int N = args.length;
if (N == 0) {
return target;
} else if (N == 1) {
String arg = ScriptRuntime.toString(args[0]);
return target.concat(arg);
}
// Find total capacity for the final string to avoid unnecessary
// re-allocations in StringBuilder
int size = target.length();
String[] argsAsStrings = new String[N];
for (int i = 0; i != N; ++i) {
String s = ScriptRuntime.toString(args[i]);
argsAsStrings[i] = s;
size += s.length();
}
StringBuilder result = new StringBuilder(size);
result.append(target);
for (int i = 0; i != N; ++i) {
result.append(argsAsStrings[i]);
}
return result.toString();
}
private static Object js_slice(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence target =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "slice"));
double begin = args.length < 1 ? 0 : ScriptRuntime.toInteger(args[0]);
double end;
int length = target.length();
if (begin < 0) {
begin += length;
if (begin < 0) begin = 0;
} else if (begin > length) {
begin = length;
}
if (args.length < 2 || args[1] == Undefined.instance) {
end = length;
} else {
end = ScriptRuntime.toInteger(args[1]);
if (end < 0) {
end += length;
if (end < 0) end = 0;
} else if (end > length) {
end = length;
}
if (end < begin) end = begin;
}
return target.subSequence((int) begin, (int) end);
}
private static Object js_at(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str = ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "at"));
Object targetArg = (args.length >= 1) ? args[0] : Undefined.instance;
int len = str.length();
int relativeIndex = (int) ScriptRuntime.toInteger(targetArg);
int k = (relativeIndex >= 0) ? relativeIndex : len + relativeIndex;
if ((k < 0) || (k >= len)) {
return Undefined.instance;
}
return str.substring(k, k + 1);
}
private static Object js_equals(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String s1 = ScriptRuntime.toString(thisObj);
String s2 = ScriptRuntime.toString(args, 0);
return s1.equals(s2);
}
private static Object js_equalsIgnoreCase(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String s1 = ScriptRuntime.toString(thisObj);
String s2 = ScriptRuntime.toString(args, 0);
return s1.equalsIgnoreCase(s2);
}
private static Object js_search(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "search");
Object regexp = args.length > 0 ? args[0] : Undefined.instance;
RegExpProxy regExpProxy = ScriptRuntime.checkRegExpProxy(cx);
if (regexp != null && !Undefined.isUndefined(regexp)) {
Object matcher = ScriptRuntime.getObjectElem(regexp, SymbolKey.SEARCH, cx, scope);
// If method is not undefined, it should be a Callable
if (matcher != null && !Undefined.isUndefined(matcher)) {
if (!(matcher instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
regexp, matcher, SymbolKey.SEARCH.getName());
}
return ((Callable) matcher)
.call(cx, scope, ScriptRuntime.toObject(scope, regexp), new Object[] {o});
}
}
String s = ScriptRuntime.toString(o);
String regexpToString = Undefined.isUndefined(regexp) ? "" : ScriptRuntime.toString(regexp);
String flags = null;
// Not standard; Done for backward compatibility
if (cx.getLanguageVersion() < Context.VERSION_1_6 && args.length > 1) {
flags = ScriptRuntime.toString(args[1]);
}
Object compiledRegExp = regExpProxy.compileRegExp(cx, regexpToString, flags);
Scriptable rx = regExpProxy.wrapRegExp(cx, scope, compiledRegExp);
Object method = ScriptRuntime.getObjectElem(rx, SymbolKey.SEARCH, cx, scope);
if (!(method instanceof Callable)) {
throw ScriptRuntime.notFunctionError(rx, method, SymbolKey.SEARCH.getName());
}
return ((Callable) method).call(cx, scope, rx, new Object[] {s});
}
private static Object js_replace(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMAScript spec 22.1.3.19
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "replace");
if (cx.getLanguageVersion() <= Context.VERSION_1_8) {
// Use old algorithm for backward compatibility
return ScriptRuntime.checkRegExpProxy(cx)
.action(cx, scope, thisObj, args, RegExpProxy.RA_REPLACE);
}
// Spec-compliant algorithm
Object searchValue = args.length > 0 ? args[0] : Undefined.instance;
Object replaceValue = args.length > 1 ? args[1] : Undefined.instance;
if (!Undefined.isUndefined(searchValue) && searchValue != null) {
Object replacer =
ScriptRuntime.getObjectElem(searchValue, SymbolKey.REPLACE, cx, scope);
// If method is not undefined, it should be a Callable
if (replacer != null && !Undefined.isUndefined(replacer)) {
if (!(replacer instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
searchValue, replacer, SymbolKey.REPLACE.getName());
}
return ((Callable) replacer)
.call(
cx,
scope,
ScriptRuntime.toObject(scope, searchValue),
new Object[] {
o instanceof NativeString ? ((NativeString) o).string : o,
replaceValue
});
}
}
String string = ScriptRuntime.toString(o);
String searchString = ScriptRuntime.toString(searchValue);
boolean functionalReplace = replaceValue instanceof Callable;
if (!functionalReplace) {
replaceValue = ScriptRuntime.toString(replaceValue);
}
int searchLength = searchString.length();
int position = string.indexOf(searchString);
if (position == -1) {
return string;
}
String preceding = string.substring(0, position);
String following = string.substring(position + searchLength);
String replacement;
if (functionalReplace) {
Scriptable callThis =
ScriptRuntime.getApplyOrCallThis(cx, scope, null, 0, (Callable) replaceValue);
Object replacementObj =
((Callable) replaceValue)
.call(
cx,
scope,
callThis,
new Object[] {
searchString, position, string,
});
replacement = ScriptRuntime.toString(replacementObj);
} else {
NativeArray captures = (NativeArray) cx.newArray(scope, 0);
replacement =
AbstractEcmaStringOperations.getSubstitution(
cx,
scope,
searchString,
string,
position,
captures,
Undefined.SCRIPTABLE_UNDEFINED,
(String) replaceValue);
}
return preceding + replacement + following;
}
private static Object js_replaceAll(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMAScript spec 22.1.3.20
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "replaceAll");
Object searchValue = args.length > 0 ? args[0] : Undefined.instance;
Object replaceValue = args.length > 1 ? args[1] : Undefined.instance;
if (searchValue != null && !Undefined.isUndefined(searchValue)) {
boolean isRegExp =
searchValue instanceof Scriptable
&& AbstractEcmaObjectOperations.isRegExp(cx, scope, searchValue);
if (isRegExp) {
Object flags = ScriptRuntime.getObjectProp(searchValue, "flags", cx, scope);
requireObjectCoercible(cx, flags, CLASS_NAME, "replaceAll");
String flagsStr = ScriptRuntime.toString(flags);
if (!flagsStr.contains("g")) {
throw ScriptRuntime.typeErrorById("msg.str.replace.all.no.global.flag");
}
}
Object matcher = ScriptRuntime.getObjectElem(searchValue, SymbolKey.REPLACE, cx, scope);
// If method is not undefined, it should be a Callable
if (matcher != null && !Undefined.isUndefined(matcher)) {
if (!(matcher instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
searchValue, matcher, SymbolKey.REPLACE.getName());
}
return ((Callable) matcher)
.call(
cx,
scope,
ScriptRuntime.toObject(scope, searchValue),
new Object[] {o, replaceValue});
}
}
String string = ScriptRuntime.toString(o);
String searchString = ScriptRuntime.toString(searchValue);
boolean functionalReplace = replaceValue instanceof Callable;
if (!functionalReplace) {
replaceValue = ScriptRuntime.toString(replaceValue);
}
int searchLength = searchString.length();
int advanceBy = Math.max(1, searchLength);
List<Integer> matchPositions = new ArrayList<>();
int position = string.indexOf(searchString);
while (position != -1) {
matchPositions.add(position);
int newPosition = string.indexOf(searchString, position + advanceBy);
if (newPosition == position) {
break;
}
position = newPosition;
}
int endOfLastMatch = 0;
StringBuilder result = new StringBuilder();
for (Integer p : matchPositions) {
String preserved = string.substring(endOfLastMatch, p);
String replacement;
if (functionalReplace) {
Scriptable callThis =
ScriptRuntime.getApplyOrCallThis(
cx, scope, null, 0, (Callable) replaceValue);
Object replacementObj =
((Callable) replaceValue)
.call(
cx,
scope,
callThis,
new Object[] {
searchString, p, string,
});
replacement = ScriptRuntime.toString(replacementObj);
} else {
NativeArray captures = (NativeArray) cx.newArray(scope, 0);
replacement =
AbstractEcmaStringOperations.getSubstitution(
cx,
scope,
searchString,
string,
p,
captures,
Undefined.SCRIPTABLE_UNDEFINED,
(String) replaceValue);
}
result.append(preserved);
result.append(replacement);
endOfLastMatch = p + searchLength;
}
if (endOfLastMatch < string.length()) {
result.append(string.substring(endOfLastMatch));
}
return result.toString();
}
private static Object js_matchAll(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// See ECMAScript spec 22.1.3.14
Object o = requireObjectCoercible(cx, thisObj, CLASS_NAME, "matchAll");
Object regexp = args.length > 0 ? args[0] : Undefined.instance;
if (regexp != null && !Undefined.isUndefined(regexp)) {
boolean isRegExp = AbstractEcmaObjectOperations.isRegExp(cx, scope, regexp);
if (isRegExp) {
Object flags = ScriptRuntime.getObjectProp(regexp, "flags", cx, scope);
requireObjectCoercible(cx, flags, CLASS_NAME, "matchAll");
String flagsStr = ScriptRuntime.toString(flags);
if (!flagsStr.contains("g")) {
throw ScriptRuntime.typeErrorById("msg.str.match.all.no.global.flag");
}
}
Object matcher = ScriptRuntime.getObjectElem(regexp, SymbolKey.MATCH_ALL, cx, scope);
// If method is not undefined, it should be a Callable
if (matcher != null && !Undefined.isUndefined(matcher)) {
if (!(matcher instanceof Callable)) {
throw ScriptRuntime.notFunctionError(
regexp, matcher, SymbolKey.MATCH_ALL.getName());
}
return ((Callable) matcher)
.call(cx, scope, ScriptRuntime.toObject(scope, regexp), new Object[] {o});
}
}
String s = ScriptRuntime.toString(o);
String regexpToString = Undefined.isUndefined(regexp) ? "" : ScriptRuntime.toString(regexp);
RegExpProxy regExpProxy = ScriptRuntime.checkRegExpProxy(cx);
Object compiledRegExp = regExpProxy.compileRegExp(cx, regexpToString, "g");
Scriptable rx = regExpProxy.wrapRegExp(cx, scope, compiledRegExp);
Object method = ScriptRuntime.getObjectElem(rx, SymbolKey.MATCH_ALL, cx, scope);
if (!(method instanceof Callable)) {
throw ScriptRuntime.notFunctionError(rx, method, SymbolKey.MATCH_ALL.getName());
}
return ((Callable) method).call(cx, scope, rx, new Object[] {s});
}
private static Object js_localeCompare(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
// For now, create and configure a collator instance. I can't
// actually imagine that this'd be slower than caching them
// a la ClassCache, so we aren't trying to outsmart ourselves
// with a caching mechanism for now.
String thisStr =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "localeCompare"));
Collator collator = Collator.getInstance(cx.getLocale());
collator.setStrength(Collator.IDENTICAL);
collator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
return collator.compare(thisStr, ScriptRuntime.toString(args, 0));
}
private static Object js_toLocaleLowerCase(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String thisStr =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "toLocaleLowerCase"));
Locale locale = cx.getLocale();
if (args.length > 0 && cx.hasFeature(Context.FEATURE_INTL_402)) {
String lang = ScriptRuntime.toString(args[0]);
locale = new Locale(lang);
}
return thisStr.toLowerCase(locale);
}
private static Object js_toLocaleUpperCase(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String thisStr =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "toLocaleUpperCase"));
Locale locale = cx.getLocale();
if (args.length > 0 && cx.hasFeature(Context.FEATURE_INTL_402)) {
String lang = ScriptRuntime.toString(args[0]);
locale = new Locale(lang);
}
return thisStr.toUpperCase(locale);
}
private static Object js_trim(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "trim"));
char[] chars = str.toCharArray();
int start = 0;
while (start < chars.length && ScriptRuntime.isJSWhitespaceOrLineTerminator(chars[start])) {
start++;
}
int end = chars.length;
while (end > start && ScriptRuntime.isJSWhitespaceOrLineTerminator(chars[end - 1])) {
end--;
}
return str.substring(start, end);
}
private static Object js_trimLeft(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "trimLeft"));
char[] chars = str.toCharArray();
int start = 0;
while (start < chars.length && ScriptRuntime.isJSWhitespaceOrLineTerminator(chars[start])) {
start++;
}
int end = chars.length;
return str.substring(start, end);
}
private static Object js_trimRight(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "trimRight"));
char[] chars = str.toCharArray();
int start = 0;
int end = chars.length;
while (end > start && ScriptRuntime.isJSWhitespaceOrLineTerminator(chars[end - 1])) {
end--;
}
return str.substring(start, end);
}
private static Object js_normalize(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
if (args.length == 0 || Undefined.isUndefined(args[0])) {
return Normalizer.normalize(
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "normalize")),
Normalizer.Form.NFC);
}
final String formStr = ScriptRuntime.toString(args, 0);
final Normalizer.Form form;
if (Normalizer.Form.NFD.name().equals(formStr)) form = Normalizer.Form.NFD;
else if (Normalizer.Form.NFKC.name().equals(formStr)) form = Normalizer.Form.NFKC;
else if (Normalizer.Form.NFKD.name().equals(formStr)) form = Normalizer.Form.NFKD;
else if (Normalizer.Form.NFC.name().equals(formStr)) form = Normalizer.Form.NFC;
else
throw rangeError(
"The normalization form should be one of 'NFC', 'NFD', 'NFKC', 'NFKD'.");
return Normalizer.normalize(
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "normalize")),
form);
}
private static String js_repeat(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str =
ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, CLASS_NAME, "repeat"));
double cnt = ScriptRuntime.toInteger(args, 0);
if ((cnt < 0.0) || (cnt == Double.POSITIVE_INFINITY)) {
throw rangeError("Invalid count value");
}
if (cnt == 0.0 || str.isEmpty()) {
return "";
}
long size = str.length() * (long) cnt;
// Check for overflow
if ((cnt > Integer.MAX_VALUE) || (size > Integer.MAX_VALUE)) {
throw rangeError("Invalid size or count value");
}
StringBuilder retval = new StringBuilder((int) size);
retval.append(str);
int i = 1;
int icnt = (int) cnt;
while (i <= (icnt / 2)) {
retval.append(retval);
i *= 2;
}
if (i < icnt) {
retval.append(retval.substring(0, str.length() * (icnt - i)));
}
return retval.toString();
}
private static Object js_codePointAt(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
String str =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "codePointAt"));
double cnt = ScriptRuntime.toInteger(args, 0);
return (cnt < 0 || cnt >= str.length()) ? Undefined.instance : str.codePointAt((int) cnt);
}
/**
* @see <a
* href="https://www.ecma-international.org/ecma-262/8.0/#sec-string.prototype.padstart">21.1.3.14String.prototype.padStart(maxLength[,
* fillString])</a>
* @see <a
* href="https://www.ecma-international.org/ecma-262/8.0/#sec-string.prototype.padend">21.1.3.13String.prototype.padEnd(maxLength[,
* fillString])</a>
*/
private static String pad(
Context cx, Scriptable thisObj, String functionName, Object[] args, boolean atStart) {
String pad =
ScriptRuntime.toString(
requireObjectCoercible(cx, thisObj, CLASS_NAME, functionName));
long intMaxLength = ScriptRuntime.toLength(args, 0);
if (intMaxLength <= pad.length()) {
return pad;
}
String filler = " ";
if (args.length >= 2 && !Undefined.isUndefined(args[1])) {
filler = ScriptRuntime.toString(args[1]);
if (filler.isEmpty()) {
return pad;
}
}
// cast is not really correct here
int fillLen = (int) (intMaxLength - pad.length());
StringBuilder concat = new StringBuilder();
do {
concat.append(filler);
} while (concat.length() < fillLen);
concat.setLength(fillLen);
if (atStart) {
return concat.append(pad).toString();
}
return concat.insert(0, pad).toString();
}
private static Object js_padStart(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return pad(cx, thisObj, "padStart", args, true);
}
private static Object js_padEnd(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return pad(cx, thisObj, "padEnd", args, false);
}
/**
*
*
* <h1>String.raw (template, ...substitutions)</h1>
*
* <p>22.1.2.4 String.raw [Draft ECMA-262 / April 28, 2021]
*/
private static Object js_raw(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
/* step 1-2 */
Object arg0 = args.length > 0 ? args[0] : Undefined.instance;
Scriptable cooked = ScriptRuntime.toObject(cx, scope, arg0);
/* step 3 */
Object rawValue = ScriptRuntime.getObjectProp(cooked, "raw", cx);
Scriptable raw = ScriptRuntime.toObject(cx, scope, rawValue);
/* step 4-5 */
long rawLength = NativeArray.getLengthProperty(cx, raw);
if (rawLength > Integer.MAX_VALUE) {
throw ScriptRuntime.rangeError("raw.length > " + Integer.MAX_VALUE);
}
int literalSegments = (int) rawLength;
if (literalSegments <= 0) return "";
/* step 6-7 */
StringBuilder elements = new StringBuilder();
int nextIndex = 0;
for (; ; ) {
/* step 8 a-i */
Object next;
next = ScriptRuntime.getObjectIndex(raw, nextIndex, cx);
String nextSeg = ScriptRuntime.toString(next);
elements.append(nextSeg);
nextIndex += 1;
if (nextIndex == literalSegments) {
break;
}
if (args.length > nextIndex) {
next = args[nextIndex];
String nextSub = ScriptRuntime.toString(next);
elements.append(nextSub);
}
}
return elements;
}
private static Object js_isWellFormed(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence str =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "isWellFormed"));
int len = str.length();
boolean foundLeadingSurrogate = false;
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (NativeJSON.isLeadingSurrogate(c)) {
if (foundLeadingSurrogate) {
return false;
}
foundLeadingSurrogate = true;
} else if (NativeJSON.isTrailingSurrogate(c)) {
if (!foundLeadingSurrogate) {
return false;
}
foundLeadingSurrogate = false;
} else if (foundLeadingSurrogate) {
return false;
}
}
return !foundLeadingSurrogate;
}
private static Object js_toWellFormed(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
CharSequence str =
ScriptRuntime.toCharSequence(
requireObjectCoercible(cx, thisObj, CLASS_NAME, "toWellFormed"));
// true represents a surrogate pair
// false represents a singular surrogate
// normal characters aren't present
Map<Integer, Boolean> surrogates = new HashMap<>();
int len = str.length();
char prev = 0;
int firstSurrogateIndex = -1;
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (NativeJSON.isLeadingSurrogate(prev) && NativeJSON.isTrailingSurrogate(c)) {
surrogates.put(i - 1, Boolean.TRUE);
surrogates.put(i, Boolean.TRUE);
} else if (NativeJSON.isLeadingSurrogate(c) || NativeJSON.isTrailingSurrogate(c)) {
surrogates.put(i, Boolean.FALSE);
if (firstSurrogateIndex == -1) {
firstSurrogateIndex = i;
}
}
prev = c;
}
if (surrogates.isEmpty()) {
return str.toString();
}
StringBuilder sb = new StringBuilder(str.subSequence(0, firstSurrogateIndex));
for (int i = firstSurrogateIndex; i < len; i++) {
char c = str.charAt(i);
Boolean pairOrNormal = surrogates.get(i);
if (pairOrNormal == null || pairOrNormal) {
sb.append(c);
} else {
sb.append('\uFFFD');
}
}
return sb.toString();
}
private static Object js_bold(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "bold", "b", null, args);
}
private static Object js_italics(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "italics", "i", null, args);
}
private static Object js_fixed(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "fixed", "tt", null, args);
}
private static Object js_strike(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "strike", "strike", null, args);
}
private static Object js_small(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "small", "small", null, args);
}
private static Object js_big(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "big", "big", null, args);
}
private static Object js_blink(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "blink", "blink", null, args);
}
private static Object js_sup(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "sup", "sup", null, args);
}
private static Object js_sub(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "sub", "sub", null, args);
}
private static Object js_fontsize(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "fontsize", "font", "size", args);
}
private static Object js_fontcolor(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "fontcolor", "font", "color", args);
}
private static Object js_link(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "link", "a", "href", args);
}
private static Object js_anchor(
Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
return tagify(cx, thisObj, "anchor", "a", "name", args);
}
}