feat: implement string polyfill in java

This commit is contained in:
TopchetoEU 2023-09-25 18:18:36 +03:00
parent 4aaf2f26db
commit 47c62128ab
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
6 changed files with 259 additions and 304 deletions

View File

@ -11,6 +11,7 @@ interface Internals {
promise: PromiseConstructor; promise: PromiseConstructor;
bool: BooleanConstructor; bool: BooleanConstructor;
number: NumberConstructor; number: NumberConstructor;
string: StringConstructor;
markSpecial(...funcs: Function[]): void; markSpecial(...funcs: Function[]): void;
getEnv(func: Function): Environment | undefined; getEnv(func: Function): Environment | undefined;
@ -50,11 +51,13 @@ try {
var Promise = env.global.Promise = internals.promise; var Promise = env.global.Promise = internals.promise;
var Boolean = env.global.Boolean = internals.bool; var Boolean = env.global.Boolean = internals.bool;
var Number = env.global.Number = internals.number; var Number = env.global.Number = internals.number;
var String = env.global.String = internals.string;
env.setProto('object', Object.prototype); env.setProto('object', Object.prototype);
env.setProto('function', Function.prototype); env.setProto('function', Function.prototype);
env.setProto('array', Array.prototype); env.setProto('array', Array.prototype);
env.setProto('number', Number.prototype); env.setProto('number', Number.prototype);
env.setProto('string', String.prototype);
(Object.prototype as any).__proto__ = null; (Object.prototype as any).__proto__ = null;
@ -64,7 +67,6 @@ try {
run('values/symbol'); run('values/symbol');
run('values/errors'); run('values/errors');
run('values/string'); run('values/string');
// run('values/number');
run('map'); run('map');
run('set'); run('set');
run('regex'); run('regex');

View File

@ -5,8 +5,6 @@
"utils.ts", "utils.ts",
"values/symbol.ts", "values/symbol.ts",
"values/errors.ts", "values/errors.ts",
"values/string.ts",
"values/number.ts",
"map.ts", "map.ts",
"set.ts", "set.ts",
"regex.ts", "regex.ts",

View File

@ -1,33 +0,0 @@
define("values/number", () => {
var Number = env.global.Number = function(this: Number | undefined, arg: any) {
var val;
if (arguments.length === 0) val = 0;
else val = arg - 0;
if (this === undefined || this === null) return val;
else (this as any).value = val;
} as NumberConstructor;
env.setProto('number', Number.prototype);
setConstr(Number.prototype, Number);
setProps(Number.prototype, {
valueOf() {
if (typeof this === 'number') return this;
else return (this as any).value;
},
toString() {
if (typeof this === 'number') return this + '';
else return (this as any).value + '';
}
});
setProps(Number, {
parseInt(val) { return Math.trunc(val as any - 0); },
parseFloat(val) { return val as any - 0; },
});
env.global.parseInt = Number.parseInt;
env.global.parseFloat = Number.parseFloat;
env.global.Object.defineProperty(env.global, 'NaN', { value: 0 / 0, writable: false });
env.global.Object.defineProperty(env.global, 'Infinity', { value: 1 / 0, writable: false });
});

View File

@ -1,267 +0,0 @@
define("values/string", () => {
var String = env.global.String = function(this: String | undefined, arg: any) {
var val;
if (arguments.length === 0) val = '';
else val = arg + '';
if (this === undefined || this === null) return val;
else (this as any).value = val;
} as StringConstructor;
env.setProto('string', String.prototype);
setConstr(String.prototype, String);
setProps(String.prototype, {
toString() {
if (typeof this === 'string') return this;
else return (this as any).value;
},
valueOf() {
if (typeof this === 'string') return this;
else return (this as any).value;
},
substring(start, end) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.substring(start, end);
else throw new Error('This function may be used only with primitive or object strings.');
}
start = start ?? 0 | 0;
end = (end ?? this.length) | 0;
const res = [];
for (let i = start; i < end; i++) {
if (i >= 0 && i < this.length) res[res.length] = this[i];
}
return internals.stringFromStrings(res);
},
substr(start, length) {
start = start ?? 0 | 0;
if (start >= this.length) start = this.length - 1;
if (start < 0) start = 0;
length = (length ?? this.length - start) | 0;
const end = length + start;
const res = [];
for (let i = start; i < end; i++) {
if (i >= 0 && i < this.length) res[res.length] = this[i];
}
return internals.stringFromStrings(res);
},
toLowerCase() {
// TODO: Implement localization
const res = [];
for (let i = 0; i < this.length; i++) {
const c = internals.char(this[i]);
if (c >= 65 && c <= 90) res[i] = c - 65 + 97;
else res[i] = c;
}
return internals.stringFromChars(res);
},
toUpperCase() {
// TODO: Implement localization
const res = [];
for (let i = 0; i < this.length; i++) {
const c = internals.char(this[i]);
if (c >= 97 && c <= 122) res[i] = c - 97 + 65;
else res[i] = c;
}
return internals.stringFromChars(res);
},
charAt(pos) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.charAt(pos);
else throw new Error('This function may be used only with primitive or object strings.');
}
pos = pos | 0;
if (pos < 0 || pos >= this.length) return '';
return this[pos];
},
charCodeAt(pos) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.charAt(pos);
else throw new Error('This function may be used only with primitive or object strings.');
}
pos = pos | 0;
if (pos < 0 || pos >= this.length) return 0 / 0;
return internals.char(this[pos]);
},
startsWith(term, pos) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.startsWith(term, pos);
else throw new Error('This function may be used only with primitive or object strings.');
}
pos = pos! | 0;
term = term + "";
if (pos < 0 || this.length < term.length + pos) return false;
for (let i = 0; i < term.length; i++) {
if (this[i + pos] !== term[i]) return false;
}
return true;
},
endsWith(term, pos) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.endsWith(term, pos);
else throw new Error('This function may be used only with primitive or object strings.');
}
pos = (pos ?? this.length) | 0;
term = term + "";
const start = pos - term.length;
if (start < 0 || this.length < term.length + start) return false;
for (let i = 0; i < term.length; i++) {
if (this[i + start] !== term[i]) return false;
}
return true;
},
indexOf(term: any, start) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.indexOf(term, start);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof term[env.global.Symbol.search] !== 'function') term = RegExp.escape(term);
return term[env.global.Symbol.search](this, false, start);
},
lastIndexOf(term: any, start) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.indexOf(term, start);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof term[env.global.Symbol.search] !== 'function') term = RegExp.escape(term);
return term[env.global.Symbol.search](this, true, start);
},
includes(term, start) {
return this.indexOf(term, start) >= 0;
},
replace(pattern: any, val) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.replace(pattern, val);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof pattern[env.global.Symbol.replace] !== 'function') pattern = RegExp.escape(pattern);
return pattern[env.global.Symbol.replace](this, val);
},
replaceAll(pattern: any, val) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.replace(pattern, val);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof pattern[env.global.Symbol.replace] !== 'function') pattern = RegExp.escape(pattern, "g");
if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g");
return pattern[env.global.Symbol.replace](this, val);
},
match(pattern: any) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.match(pattern);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof pattern[env.global.Symbol.match] !== 'function') pattern = RegExp.escape(pattern);
return pattern[env.global.Symbol.match](this);
},
matchAll(pattern: any) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.matchAll(pattern);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof pattern[env.global.Symbol.match] !== 'function') pattern = RegExp.escape(pattern, "g");
if (pattern instanceof RegExp && !pattern.global) pattern = new pattern.constructor(pattern.source, pattern.flags + "g");
return pattern[env.global.Symbol.match](this);
},
split(pattern: any, lim, sensible) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.split(pattern, lim, sensible);
else throw new Error('This function may be used only with primitive or object strings.');
}
if (typeof pattern[env.global.Symbol.split] !== 'function') pattern = RegExp.escape(pattern, "g");
return pattern[env.global.Symbol.split](this, lim, sensible);
},
slice(start, end) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.slice(start, end);
else throw new Error('This function may be used only with primitive or object strings.');
}
start = wrapI(this.length, start ?? 0 | 0);
end = wrapI(this.length, end ?? this.length | 0);
if (start > end) return '';
return this.substring(start, end);
},
concat(...args) {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.concat(...args);
else throw new Error('This function may be used only with primitive or object strings.');
}
var res = this;
for (var arg of args) res += arg;
return res;
},
trim() {
return this
.replace(/^\s+/g, '')
.replace(/\s+$/g, '');
}
});
setProps(String, {
fromCharCode(val) {
return internals.stringFromChars([val | 0]);
},
})
env.global.Object.defineProperty(String.prototype, 'length', {
get() {
if (typeof this !== 'string') {
if (this instanceof String) return (this as any).value.length;
else throw new Error('This function may be used only with primitive or object strings.');
}
return internals.strlen(this);
},
configurable: true,
enumerable: false,
});
});

View File

@ -14,7 +14,7 @@ import me.topchetoeu.jscript.interop.Native;
public class Internals { public class Internals {
public final Environment targetEnv; public final Environment targetEnv;
@Native public final FunctionValue object, function, promise, array, bool, number; @Native public final FunctionValue object, function, promise, array, bool, number, string;
@Native public void markSpecial(FunctionValue ...funcs) { @Native public void markSpecial(FunctionValue ...funcs) {
for (var func : funcs) { for (var func : funcs) {
@ -159,5 +159,6 @@ public class Internals {
this.array = targetEnv.wrappersProvider.getConstr(ArrayPolyfill.class); this.array = targetEnv.wrappersProvider.getConstr(ArrayPolyfill.class);
this.bool = targetEnv.wrappersProvider.getConstr(BooleanPolyfill.class); this.bool = targetEnv.wrappersProvider.getConstr(BooleanPolyfill.class);
this.number = targetEnv.wrappersProvider.getConstr(NumberPolyfill.class); this.number = targetEnv.wrappersProvider.getConstr(NumberPolyfill.class);
this.string = targetEnv.wrappersProvider.getConstr(StringPolyfill.class);
} }
} }

View File

@ -0,0 +1,254 @@
package me.topchetoeu.jscript.polyfills;
import java.util.regex.Pattern;
import me.topchetoeu.jscript.engine.Context;
import me.topchetoeu.jscript.engine.values.ArrayValue;
import me.topchetoeu.jscript.engine.values.FunctionValue;
import me.topchetoeu.jscript.engine.values.ObjectValue;
import me.topchetoeu.jscript.engine.values.Values;
import me.topchetoeu.jscript.exceptions.EngineException;
import me.topchetoeu.jscript.interop.Native;
import me.topchetoeu.jscript.interop.NativeConstructor;
import me.topchetoeu.jscript.interop.NativeGetter;
// TODO: implement index wrapping properly
public class StringPolyfill {
public final String value;
private static String passThis(String funcName, Object val) {
if (val instanceof StringPolyfill) return ((StringPolyfill)val).value;
else if (val instanceof String) return (String)val;
else throw EngineException.ofType(String.format("'%s' may be called upon object and primitve strings.", funcName));
}
private static int normalizeI(int i, int len, boolean clamp) {
if (i < 0) i += len;
if (clamp) {
if (i < 0) i = 0;
if (i >= len) i = len;
}
return i;
}
@NativeGetter(thisArg = true) public static int length(Context ctx, Object thisArg) {
return passThis("substring", thisArg).length();
}
@Native(thisArg = true) public static String substring(Context ctx, Object thisArg, int start, Object _end) throws InterruptedException {
var val = passThis("substring", thisArg);
start = normalizeI(start, val.length(), true);
int end = normalizeI(_end == null ? val.length() : (int)Values.toNumber(ctx, _end), val.length(), true);
return val.substring(start, end);
}
@Native(thisArg = true) public static String substr(Context ctx, Object thisArg, int start, Object _len) throws InterruptedException {
var val = passThis("substr", thisArg);
int len = _len == null ? val.length() - start : (int)Values.toNumber(ctx, _len);
return substring(ctx, val, start, start + len);
}
@Native(thisArg = true) public static String toLowerCase(Context ctx, Object thisArg) {
return passThis("toLowerCase", thisArg).toLowerCase();
}
@Native(thisArg = true) public static String toUpperCase(Context ctx, Object thisArg) {
return passThis("toUpperCase", thisArg).toUpperCase();
}
@Native(thisArg = true) public static String charAt(Context ctx, Object thisArg, int i) {
return passThis("charAt", thisArg).charAt(i) + "";
}
@Native(thisArg = true) public static int charCodeAt(Context ctx, Object thisArg, int i) {
return passThis("charCodeAt", thisArg).charAt(i);
}
@Native(thisArg = true) public static boolean startsWith(Context ctx, Object thisArg, String term, int pos) {
return passThis("startsWith", thisArg).startsWith(term, pos);
}
@Native(thisArg = true) public static boolean endsWith(Context ctx, Object thisArg, String term, int pos) throws InterruptedException {
var val = passThis("endsWith", thisArg);
return val.lastIndexOf(term, pos) >= 0;
}
@Native(thisArg = true) public static int indexOf(Context ctx, Object thisArg, Object term, int start) throws InterruptedException {
var val = passThis("indexOf", thisArg);
if (term != null && term != Values.NULL && !(term instanceof String)) {
var search = Values.getMember(ctx, term, ctx.env.symbol("Symbol.search"));
if (search instanceof FunctionValue) {
return (int)Values.toNumber(ctx, ((FunctionValue)search).call(ctx, term, val, false, start));
}
}
return val.indexOf(Values.toString(ctx, term), start);
}
@Native(thisArg = true) public static int lastIndexOf(Context ctx, Object thisArg, Object term, int pos) throws InterruptedException {
var val = passThis("lastIndexOf", thisArg);
if (term != null && term != Values.NULL && !(term instanceof String)) {
var search = Values.getMember(ctx, term, ctx.env.symbol("Symbol.search"));
if (search instanceof FunctionValue) {
return (int)Values.toNumber(ctx, ((FunctionValue)search).call(ctx, term, val, true, pos));
}
}
return val.lastIndexOf(Values.toString(ctx, term), pos);
}
@Native(thisArg = true) public static boolean includes(Context ctx, Object thisArg, Object term, int pos) throws InterruptedException {
return lastIndexOf(ctx, passThis("includes", thisArg), term, pos) >= 0;
}
@Native(thisArg = true) public static String replace(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException {
var val = passThis("replace", thisArg);
if (term != null && term != Values.NULL && !(term instanceof String)) {
var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace"));
if (replace instanceof FunctionValue) {
return Values.toString(ctx, ((FunctionValue)replace).call(ctx, term, val, replacement));
}
}
return val.replaceFirst(Pattern.quote(Values.toString(ctx, term)), replacement);
}
@Native(thisArg = true) public static String replaceAll(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException {
var val = passThis("replaceAll", thisArg);
if (term != null && term != Values.NULL && !(term instanceof String)) {
var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace"));
if (replace instanceof FunctionValue) {
return Values.toString(ctx, ((FunctionValue)replace).call(ctx, term, val, replacement));
}
}
return val.replaceFirst(Pattern.quote(Values.toString(ctx, term)), replacement);
}
@Native(thisArg = true) public static ArrayValue match(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException {
var val = passThis("match", thisArg);
FunctionValue match;
try {
var _match = Values.getMember(ctx, term, ctx.env.symbol("Symbol.match"));
if (_match instanceof FunctionValue) match = (FunctionValue)_match;
else if (ctx.env.regexConstructor != null) {
var regex = Values.callNew(ctx, ctx.env.regexConstructor, Values.toString(ctx, term), "");
_match = Values.getMember(ctx, regex, ctx.env.symbol("Symbol.match"));
if (_match instanceof FunctionValue) match = (FunctionValue)_match;
else throw EngineException.ofError("Regular expressions don't support matching.");
}
else throw EngineException.ofError("Regular expressions not supported.");
}
catch (IllegalArgumentException e) { return new ArrayValue(ctx, ""); }
var res = match.call(ctx, term, val);
if (res instanceof ArrayValue) return (ArrayValue)res;
else return new ArrayValue(ctx, "");
}
@Native(thisArg = true) public static Object matchAll(Context ctx, Object thisArg, Object term, String replacement) throws InterruptedException {
var val = passThis("matchAll", thisArg);
FunctionValue match = null;
try {
var _match = Values.getMember(ctx, term, ctx.env.symbol("Symbol.matchAll"));
if (_match instanceof FunctionValue) match = (FunctionValue)_match;
}
catch (IllegalArgumentException e) { }
if (match == null && ctx.env.regexConstructor != null) {
var regex = Values.callNew(ctx, ctx.env.regexConstructor, Values.toString(ctx, term), "g");
var _match = Values.getMember(ctx, regex, ctx.env.symbol("Symbol.matchAll"));
if (_match instanceof FunctionValue) match = (FunctionValue)_match;
else throw EngineException.ofError("Regular expressions don't support matching.");
}
else throw EngineException.ofError("Regular expressions not supported.");
return match.call(ctx, term, val);
}
@Native(thisArg = true) public static ArrayValue split(Context ctx, Object thisArg, Object term, Object lim, boolean sensible) throws InterruptedException {
var val = passThis("split", thisArg);
if (lim != null) lim = Values.toNumber(ctx, lim);
if (term != null && term != Values.NULL && !(term instanceof String)) {
var replace = Values.getMember(ctx, term, ctx.env.symbol("Symbol.replace"));
if (replace instanceof FunctionValue) {
var tmp = ((FunctionValue)replace).call(ctx, term, val, lim, sensible);
if (tmp instanceof ArrayValue) {
var parts = new ArrayValue(((ArrayValue)tmp).size());
for (int i = 0; i < parts.size(); i++) parts.set(ctx, i, Values.toString(ctx, ((ArrayValue)tmp).get(i)));
return parts;
}
}
}
String[] parts;
var pattern = Pattern.quote(Values.toString(ctx, term));
if (lim == null) parts = val.split(pattern);
else if (sensible) parts = val.split(pattern, (int)(double)lim);
else {
var limit = (int)(double)lim;
parts = val.split(pattern, limit + 1);
ArrayValue res;
if (parts.length > limit) res = new ArrayValue(limit);
else res = new ArrayValue(parts.length);
for (var i = 0; i < parts.length; i++) res.set(ctx, i, parts[i]);
return res;
}
var res = new ArrayValue(parts.length);
var i = 0;
for (; i < parts.length; i++) {
if (lim != null && (double)lim <= i) break;
res.set(ctx, i, parts[i]);
}
return res;
}
@Native(thisArg = true) public static String slice(Context ctx, Object thisArg, int start, Object _end) throws InterruptedException {
return substring(ctx, passThis("slice", thisArg), start, _end);
}
@Native(thisArg = true) public static String concat(Context ctx, Object thisArg, Object... args) throws InterruptedException {
var res = new StringBuilder(passThis("concat", thisArg));
for (var el : args) res.append(Values.toString(ctx, el));
return res.toString();
}
@Native(thisArg = true) public static String trim(Context ctx, Object thisArg) throws InterruptedException {
return passThis("trim", thisArg).trim();
}
@NativeConstructor(thisArg = true) public static Object constructor(Context ctx, Object thisArg, Object val) throws InterruptedException {
val = Values.toString(ctx, val);
if (thisArg instanceof ObjectValue) return new StringPolyfill((String)val);
else return val;
}
@Native(thisArg = true) public static String toString(Context ctx, Object thisArg) throws InterruptedException {
return Values.toString(ctx, Values.toNumber(ctx, thisArg));
}
@Native(thisArg = true) public static String valueOf(Context ctx, Object thisArg) throws InterruptedException {
if (thisArg instanceof StringPolyfill) return ((StringPolyfill)thisArg).value;
else return Values.toString(ctx, thisArg);
}
@Native public static String fromCharCode(int ...val) {
char[] arr = new char[val.length];
for (var i = 0; i < val.length; i++) arr[i] = (char)val[i];
return new String(arr);
}
public StringPolyfill(String val) {
this.value = val;
}
}