From 47c62128ab9c1a353404a7c0858a7b3a2ca3889d Mon Sep 17 00:00:00 2001 From: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:18:36 +0300 Subject: [PATCH] feat: implement string polyfill in java --- lib/core.ts | 4 +- lib/tsconfig.json | 2 - lib/values/number.ts | 33 --- lib/values/string.ts | 267 ------------------ .../jscript/polyfills/Internals.java | 3 +- .../jscript/polyfills/StringPolyfill.java | 254 +++++++++++++++++ 6 files changed, 259 insertions(+), 304 deletions(-) delete mode 100644 lib/values/number.ts delete mode 100644 lib/values/string.ts create mode 100644 src/me/topchetoeu/jscript/polyfills/StringPolyfill.java diff --git a/lib/core.ts b/lib/core.ts index e1d0bdd..dcab864 100644 --- a/lib/core.ts +++ b/lib/core.ts @@ -11,6 +11,7 @@ interface Internals { promise: PromiseConstructor; bool: BooleanConstructor; number: NumberConstructor; + string: StringConstructor; markSpecial(...funcs: Function[]): void; getEnv(func: Function): Environment | undefined; @@ -50,11 +51,13 @@ try { var Promise = env.global.Promise = internals.promise; var Boolean = env.global.Boolean = internals.bool; var Number = env.global.Number = internals.number; + var String = env.global.String = internals.string; env.setProto('object', Object.prototype); env.setProto('function', Function.prototype); env.setProto('array', Array.prototype); env.setProto('number', Number.prototype); + env.setProto('string', String.prototype); (Object.prototype as any).__proto__ = null; @@ -64,7 +67,6 @@ try { run('values/symbol'); run('values/errors'); run('values/string'); - // run('values/number'); run('map'); run('set'); run('regex'); diff --git a/lib/tsconfig.json b/lib/tsconfig.json index 497b3a9..bf26634 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -5,8 +5,6 @@ "utils.ts", "values/symbol.ts", "values/errors.ts", - "values/string.ts", - "values/number.ts", "map.ts", "set.ts", "regex.ts", diff --git a/lib/values/number.ts b/lib/values/number.ts deleted file mode 100644 index 57e8d48..0000000 --- a/lib/values/number.ts +++ /dev/null @@ -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 }); -}); \ No newline at end of file diff --git a/lib/values/string.ts b/lib/values/string.ts deleted file mode 100644 index bfce164..0000000 --- a/lib/values/string.ts +++ /dev/null @@ -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, - }); -}); \ No newline at end of file diff --git a/src/me/topchetoeu/jscript/polyfills/Internals.java b/src/me/topchetoeu/jscript/polyfills/Internals.java index 0c13990..d37c148 100644 --- a/src/me/topchetoeu/jscript/polyfills/Internals.java +++ b/src/me/topchetoeu/jscript/polyfills/Internals.java @@ -14,7 +14,7 @@ import me.topchetoeu.jscript.interop.Native; public class Internals { 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) { for (var func : funcs) { @@ -159,5 +159,6 @@ public class Internals { this.array = targetEnv.wrappersProvider.getConstr(ArrayPolyfill.class); this.bool = targetEnv.wrappersProvider.getConstr(BooleanPolyfill.class); this.number = targetEnv.wrappersProvider.getConstr(NumberPolyfill.class); + this.string = targetEnv.wrappersProvider.getConstr(StringPolyfill.class); } } diff --git a/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java b/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java new file mode 100644 index 0000000..29f43ae --- /dev/null +++ b/src/me/topchetoeu/jscript/polyfills/StringPolyfill.java @@ -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; + } +}