/// import { BLOCK, BLOCK_OVERHEAD, BLOCK_MAXSIZE } from "./rt/common"; import { compareImpl, strtol, strtod, isSpace, isAscii, isFinalSigma, toLower8, toUpper8 } from "./util/string"; import { SPECIALS_UPPER, casemap, bsearch } from "./util/casemap"; import { E_INVALIDLENGTH } from "./util/error"; import { idof } from "./builtins"; import { Array } from "./array"; @final export abstract class String { @lazy static readonly MAX_LENGTH: i32 = (BLOCK_MAXSIZE >>> alignof()); static fromCharCode(unit: i32, surr: i32 = -1): String { var hasSur = surr > 0; var out = __alloc(2 << i32(hasSur), idof()); store(out, unit); if (hasSur) store(out, surr, 2); return changetype(out); // retains } static fromCharCodes(units: Array): String { var length = units.length; var out = __alloc(length << 1, idof()); var ptr = units.dataStart; for (let i = 0; i < length; ++i) { store(out + (i << 1), load(ptr + (i << 2))); } return changetype(out); } static fromCodePoint(code: i32): String { assert(code <= 0x10FFFF); var hasSur = code > 0xFFFF; var out = __alloc(2 << i32(hasSur), idof()); if (!hasSur) { store(out, code); } else { code -= 0x10000; let hi = (code & 0x03FF) | 0xDC00; let lo = (code >>> 10) | 0xD800; store(out, lo | (hi << 16)); } return changetype(out); // retains } get length(): i32 { return changetype(changetype(this) - BLOCK_OVERHEAD).rtSize >> 1; } @operator("[]") charAt(pos: i32): String { if (pos >= this.length) return changetype(""); var out = __alloc(2, idof()); store(out, load(changetype(this) + (pos << 1))); return changetype(out); // retains } charCodeAt(pos: i32): i32 { if (pos >= this.length) return -1; // (NaN) return load(changetype(this) + (pos << 1)); } codePointAt(pos: i32): i32 { var len = this.length; if (pos >= len) return -1; // (undefined) var first = load(changetype(this) + (pos << 1)); if ((first & 0xFC00) != 0xD800 || pos + 1 == len) return first; var second = load(changetype(this) + (pos << 1), 2); if ((second & 0xFC00) != 0xDC00) return first; return (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000; } @operator("+") private static __concat(left: String, right: String): String { return select(left, changetype("null"), left !== null).concat(right); } concat(other: String): String { if (other === null) other = changetype("null"); var thisSize: isize = this.length << 1; var otherSize: isize = other.length << 1; var outSize: usize = thisSize + otherSize; if (outSize == 0) return changetype(""); var out = changetype(__alloc(outSize, idof())); // retains memory.copy(changetype(out), changetype(this), thisSize); memory.copy(changetype(out) + thisSize, changetype(other), otherSize); return out; } endsWith(search: String, end: i32 = String.MAX_LENGTH): bool { if (search === null) return false; end = min(max(end, 0), this.length); var searchLength = search.length; var searchStart = end - searchLength; if (searchStart < 0) return false; // @ts-ignore: string <-> String return !compareImpl(this, searchStart, search, 0, searchLength); } @operator("==") private static __eq(left: String | null, right: String | null): bool { if (left === right) return true; if (left === null || right === null) return false; var leftLength = left.length; if (leftLength != right.length) return false; // @ts-ignore: string <-> String return !compareImpl(left, 0, right, 0, leftLength); } @operator.prefix("!") private static __not(str: String | null): bool { return str === null || !str.length; } @operator("!=") private static __ne(left: String | null, right: String | null): bool { return !this.__eq(left, right); } @operator(">") private static __gt(left: String | null, right: String | null): bool { if (left === right || left === null || right === null) return false; var leftLength = left.length; if (!leftLength) return false; var rightLength = right.length; if (!rightLength) return true; // @ts-ignore: string <-> String return compareImpl(left, 0, right, 0, min(leftLength, rightLength)) > 0; } @operator(">=") private static __gte(left: String, right: String): bool { return !this.__lt(left, right); } @operator("<") private static __lt(left: String, right: String): bool { if (left === right || left === null || right === null) return false; var rightLength = right.length; if (!rightLength) return false; var leftLength = left.length; if (!leftLength) return true; // @ts-ignore: string <-> String return compareImpl(left, 0, right, 0, min(leftLength, rightLength)) < 0; } @operator("<=") private static __lte(left: String, right: String): bool { return !this.__gt(left, right); } includes(search: String, start: i32 = 0): bool { return this.indexOf(search, start) != -1; } indexOf(search: String, start: i32 = 0): i32 { var searchLen = search.length; if (!searchLen) return 0; var len = this.length; if (!len) return -1; var searchStart = min(max(start, 0), len); for (len -= searchLen; searchStart <= len; ++searchStart) { // @ts-ignore: string <-> String if (!compareImpl(this, searchStart, search, 0, searchLen)) return searchStart; } return -1; } lastIndexOf(search: String, start: i32 = i32.MAX_VALUE): i32 { var searchLen = search.length; if (!searchLen) return this.length; var len = this.length; if (!len) return -1; var searchStart = min(max(start, 0), len - searchLen); for (; searchStart >= 0; --searchStart) { // @ts-ignore: string <-> String if (!compareImpl(this, searchStart, search, 0, searchLen)) return searchStart; } return -1; } // TODO: implement full locale comparison with locales and Collator options localeCompare(other: String): i32 { if (other === this) return 0; // compare pointers var len: isize = this.length; var otherLen: isize = other.length; if (otherLen != len) return select(1, -1, len > otherLen); if (!otherLen) return 0; // "" == "" // @ts-ignore: string <-> String return compareImpl(this, 0, other, 0, otherLen); } startsWith(search: String, start: i32 = 0): bool { if (search === null) search = changetype("null"); var len = this.length; var searchStart = min(max(start, 0), len); var searchLength = search.length; if (searchLength + searchStart > len) return false; // @ts-ignore: string <-> String return !compareImpl(this, searchStart, search, 0, searchLength); } substr(start: i32, length: i32 = i32.MAX_VALUE): String { // legacy var intStart: isize = start; var end: isize = length; var len: isize = this.length; if (intStart < 0) intStart = max(len + intStart, 0); var size = min(max(end, 0), len - intStart) << 1; if (size <= 0) return changetype(""); var out = __alloc(size, idof()); memory.copy(out, changetype(this) + (intStart << 1), size); return changetype(out); // retains } substring(start: i32, end: i32 = i32.MAX_VALUE): String { var len: isize = this.length; var finalStart = min(max(start, 0), len); var finalEnd = min(max(end, 0), len); var fromPos = min(finalStart, finalEnd) << 1; var toPos = max(finalStart, finalEnd) << 1; var size = toPos - fromPos; if (!size) return changetype(""); if (!fromPos && toPos == len << 1) return this; var out = __alloc(size, idof()); memory.copy(out, changetype(this) + fromPos, size); return changetype(out); // retains } trim(): String { var len = this.length; var size: usize = len << 1; while (size && isSpace(load(changetype(this) + size - 2))) { size -= 2; } var offset: usize = 0; while (offset < size && isSpace(load(changetype(this) + offset))) { offset += 2; size -= 2; } if (!size) return changetype(""); if (!offset && size == len << 1) return this; var out = __alloc(size, idof()); memory.copy(out, changetype(this) + offset, size); return changetype(out); // retains } @inline trimLeft(): String { return this.trimStart(); } @inline trimRight(): String { return this.trimEnd(); } trimStart(): String { var size = this.length << 1; var offset: usize = 0; while (offset < size && isSpace(load(changetype(this) + offset))) { offset += 2; } if (!offset) return this; size -= offset; if (!size) return changetype(""); var out = __alloc(size, idof()); memory.copy(out, changetype(this) + offset, size); return changetype(out); // retains } trimEnd(): String { var originalSize = this.length << 1; var size = originalSize; while (size && isSpace(load(changetype(this) + size - 2))) { size -= 2; } if (!size) return changetype(""); if (size == originalSize) return this; var out = __alloc(size, idof()); memory.copy(out, changetype(this), size); return changetype(out); // retains } padStart(length: i32, pad: string = " "): String { var thisSize = this.length << 1; var targetSize = length << 1; var padSize = pad.length << 1; if (targetSize < thisSize || !padSize) return this; var prependSize = targetSize - thisSize; var out = __alloc(targetSize, idof()); if (prependSize > padSize) { let repeatCount = (prependSize - 2) / padSize; let restBase = repeatCount * padSize; let restSize = prependSize - restBase; memory.repeat(out, changetype(pad), padSize, repeatCount); memory.copy(out + restBase, changetype(pad), restSize); } else { memory.copy(out, changetype(pad), prependSize); } memory.copy(out + prependSize, changetype(this), thisSize); return changetype(out); // retains } padEnd(length: i32, pad: string = " "): String { var thisSize = this.length << 1; var targetSize = length << 1; var padSize = pad.length << 1; if (targetSize < thisSize || !padSize) return this; var appendSize = targetSize - thisSize; var out = __alloc(targetSize, idof()); memory.copy(out, changetype(this), thisSize); if (appendSize > padSize) { let repeatCount = (appendSize - 2) / padSize; let restBase = repeatCount * padSize; let restSize = appendSize - restBase; memory.repeat(out + thisSize, changetype(pad), padSize, repeatCount); memory.copy(out + thisSize + restBase, changetype(pad), restSize); } else { memory.copy(out + thisSize, changetype(pad), appendSize); } return changetype(out); // retains } repeat(count: i32 = 0): String { var length = this.length; // Most browsers can't handle strings 1 << 28 chars or longer if (count < 0 || length * count > (1 << 28)) { throw new RangeError(E_INVALIDLENGTH); } if (count == 0 || !length) return changetype(""); if (count == 1) return this; var out = __alloc((length * count) << 1, idof()); memory.repeat(out, changetype(this), length << 1, count); return changetype(out); // retains } replace(search: String, replacement: String): String { var len: usize = this.length; var slen: usize = search.length; if (len <= slen) { return len < slen ? this : select(replacement, this, search == this); } var index: isize = this.indexOf(search); if (~index) { let rlen: usize = replacement.length; len -= slen; let olen = len + rlen; if (olen) { let out = __alloc(olen << 1, idof()); memory.copy(out, changetype(this), index << 1); memory.copy( out + (index << 1), changetype(replacement), rlen << 1 ); memory.copy( out + ((index + rlen) << 1), changetype(this) + ((index + slen) << 1), (len - index) << 1 ); return changetype(out); } } return this; } replaceAll(search: String, replacement: String): String { var len: usize = this.length; var slen: usize = search.length; if (len <= slen) { return len < slen ? this : select(replacement, this, search == this); } var rlen: usize = replacement.length; if (!slen) { if (!rlen) return this; // Special case: 'abc'.replaceAll('', '-') -> '-a-b-c-' let out = __alloc((len + (len + 1) * rlen) << 1, idof()); memory.copy(out, changetype(replacement), rlen << 1); let offset = rlen; for (let i: usize = 0; i < len; ++i) { store( changetype(out) + (offset++ << 1), load(changetype(this) + (i << 1)) ); memory.copy( out + (offset << 1), changetype(replacement), rlen << 1 ); offset += rlen; } return changetype(out); } var prev: isize = 0, next: isize = 0; if (slen == rlen) { // Fast path when search and replacement have same length let size = len << 1; let out = __alloc(size, idof()); memory.copy(out, changetype(this), size); while (~(next = this.indexOf(search, prev))) { memory.copy(out + (next << 1), changetype(replacement), rlen << 1); prev = next + slen; } return changetype(out); } var out: usize = 0, offset: usize = 0, resLen = len; while (~(next = this.indexOf(search, prev))) { if (!out) out = __alloc(len << 1, idof()); if (offset > resLen) { let newLength = resLen << 1; out = __realloc(out, newLength << 1); resLen = newLength; } let chunk = next - prev; memory.copy( out + (offset << 1), changetype(this) + (prev << 1), chunk << 1 ); offset += chunk; memory.copy( out + (offset << 1), changetype(replacement), rlen << 1 ); offset += rlen; prev = next + slen; } if (offset) { if (offset > resLen) { let newLength = resLen << 1; out = __realloc(out, newLength << 1); resLen = newLength; } let rest = len - prev; if (rest) { memory.copy( out + (offset << 1), changetype(this) + (prev << 1), rest << 1 ); } rest += offset; if (resLen > rest) out = __realloc(out, rest << 1); return changetype(out); } return this; } slice(start: i32, end: i32 = i32.MAX_VALUE): String { var len = this.length; start = start < 0 ? max(start + len, 0) : min(start, len); end = end < 0 ? max(end + len, 0) : min(end, len); len = end - start; if (len <= 0) return changetype(""); var out = __alloc(len << 1, idof()); memory.copy(out, changetype(this) + (start << 1), len << 1); return changetype(out); // retains } split(separator: String | null = null, limit: i32 = i32.MAX_VALUE): String[] { if (!limit) return changetype>(__allocArray(0, alignof(), idof>())); // retains if (separator === null) return [this]; var length: isize = this.length; var sepLen = separator.length; if (limit < 0) limit = i32.MAX_VALUE; if (!sepLen) { if (!length) return changetype>(__allocArray(0, alignof(), idof>())); // retains // split by chars length = min(length, limit); let result = changetype>(__allocArray(length, alignof(), idof>())); // retains // @ts-ignore: cast let resultStart = result.dataStart as usize; for (let i: isize = 0; i < length; ++i) { let charStr = __alloc(2, idof()); store(charStr, load(changetype(this) + (i << 1))); store(resultStart + (i << alignof()), charStr); // result[i] = charStr if (isManaged()) __retain(charStr); } return result; } else if (!length) { let result = changetype>(__allocArray(1, alignof(), idof>())); // retains // @ts-ignore: cast store(result.dataStart as usize, changetype("")); // static "" return result; } var result = changetype>(__allocArray(0, alignof(), idof>())); // retains var end = 0, start = 0, i = 0; while (~(end = this.indexOf(separator, start))) { let len = end - start; if (len > 0) { let out = __alloc(len << 1, idof()); memory.copy(out, changetype(this) + (start << 1), len << 1); result.push(changetype(out)); } else { result.push(changetype("")); } if (++i == limit) return result; start = end + sepLen; } if (!start) { // also means: loop above didn't do anything result.push(this); return result; } var len = length - start; if (len > 0) { let out = __alloc(len << 1, idof()); memory.copy(out, changetype(this) + (start << 1), len << 1); result.push(changetype(out)); // retains } else { result.push(changetype("")); // static "" } return result; } toLowerCase(): String { var len = this.length; if (!len) return this; var codes = __alloc(len * 2 * 2, idof()); var j: usize = 0; for (let i: usize = 0; i < len; ++i, ++j) { let c = load(changetype(this) + (i << 1)); if (isAscii(c)) { store(codes + (j << 1), toLower8(c)); } else { // check and read surrogate pair if ((c - 0xD7FF < 0xDC00 - 0xD7FF) && i < len - 1) { let c1 = load(changetype(this) + (i << 1), 2); if (c1 - 0xDBFF < 0xE000 - 0xDBFF) { let c0 = c; c = (((c & 0x03FF) << 10) | (c1 & 0x03FF)) + 0x10000; ++i; if (c >= 0x20000) { store(codes + (j << 1), c0 | (c1 << 16)); ++j; continue; } } } // check special casing for lower table. It has one ently so instead lookup we just inline this. if (c == 0x0130) { // 0x0130 -> [0x0069, 0x0307] store(codes + (j << 1), (0x0307 << 16) | 0x0069); ++j; } else if (c == 0x03A3) { // 'Σ' // Σ maps to σ but except at the end of a word where it maps to ς let sigma = 0x03C3; // σ if (len > 1 && isFinalSigma(changetype(this), i, len)) { sigma = 0x03C2; // ς } store(codes + (j << 1), sigma); } else if (c - 0x24B6 <= 0x24CF - 0x24B6) { // Range 0x24B6 <= c <= 0x24CF not covered by casemap and require special early handling store(codes + (j << 1), c + 26); } else { let code = casemap(c, 0) & 0x1FFFFF; if (code < 0x10000) { store(codes + (j << 1), code); } else { // store as surrogare pair code -= 0x10000; let lo = (code >>> 10) | 0xD800; let hi = (code & 0x03FF) | 0xDC00; store(codes + (j << 1), lo | (hi << 16)); ++j; } } } } codes = __realloc(codes, j << 1); return changetype(codes); // retains } toUpperCase(): String { var len = this.length; if (!len) return this; var codes = __alloc(len * 3 * 2, idof()); var specialsPtr = changetype(SPECIALS_UPPER); var specialsLen = SPECIALS_UPPER.length; var j: usize = 0; for (let i: usize = 0; i < len; ++i, ++j) { let c = load(changetype(this) + (i << 1)); if (isAscii(c)) { store(codes + (j << 1), toUpper8(c)); } else { // check and read surrogate pair if ((c - 0xD7FF < 0xDC00 - 0xD7FF) && i < len - 1) { let c1 = load(changetype(this) + (i << 1), 2); if (c1 - 0xDBFF < 0xE000 - 0xDBFF) { let c0 = c; c = (((c & 0x03FF) << 10) | (c1 & 0x03FF)) + 0x10000; ++i; if (c >= 0x20000) { store(codes + (j << 1), c0 | (c1 << 16)); ++j; continue; } } } // Range 0x24D0 <= c <= 0x24E9 not covered by casemap and require special early handling if (c - 0x24D0 <= 0x24E9 - 0x24D0) { // monkey patch store(codes + (j << 1), c - 26); } else { let index: usize = -1; // Fast range check. See first and last rows in specialsUpper table if (c - 0x00DF <= 0xFB17 - 0x00DF) { index = bsearch(c, specialsPtr, specialsLen); } if (~index) { // load next 3 code points from row with `index` offset for specialsUpper table let ab = load(specialsPtr + (index << 1), 2); let cc = load(specialsPtr + (index << 1), 6); store(codes + (j << 1), ab, 0); store(codes + (j << 1), cc, 4); j += 1 + usize(cc != 0); } else { let code = casemap(c, 1) & 0x1FFFFF; if (code < 0x10000) { store(codes + (j << 1), code); } else { // store as surrogare pair code -= 0x10000; let lo = (code >>> 10) | 0xD800; let hi = (code & 0x03FF) | 0xDC00; store(codes + (j << 1), lo | (hi << 16)); ++j; } } } } } codes = __realloc(codes, j << 1); return changetype(codes); // retains } toString(): String { return this; } } // @ts-ignore: nolib export type string = String; export function parseInt(str: string, radix: i32 = 0): f64 { return strtol(str, radix); } export function parseFloat(str: string): f64 { return strtod(str); } // Encoding helpers export namespace String { export namespace UTF8 { export function byteLength(str: string, nullTerminated: bool = false): i32 { var strOff = changetype(str); var strEnd = strOff + changetype(changetype(str) - BLOCK_OVERHEAD).rtSize; var bufLen = i32(nullTerminated); while (strOff < strEnd) { let c1 = load(strOff); if (c1 < 128) { // @ts-ignore: cast if (nullTerminated & !c1) break; bufLen += 1; } else if (c1 < 2048) { bufLen += 2; } else { if ((c1 & 0xFC00) == 0xD800 && strOff + 2 < strEnd) { if ((load(strOff, 2) & 0xFC00) == 0xDC00) { bufLen += 4; strOff += 4; continue; } } bufLen += 3; } strOff += 2; } return bufLen; } export function encode(str: string, nullTerminated: bool = false): ArrayBuffer { var buf = __alloc(byteLength(str, nullTerminated), idof()); encodeUnsafe(changetype(str), str.length, buf, nullTerminated); return changetype(buf); // retains } // @ts-ignore: decorator @unsafe export function encodeUnsafe(str: usize, len: i32, buf: usize, nullTerminated: bool = false): usize { var strEnd = str + (len << 1); var bufOff = buf; while (str < strEnd) { let c1 = load(str); if (c1 < 128) { store(bufOff, c1); bufOff++; } else if (c1 < 2048) { let b0 = c1 >> 6 | 192; let b1 = c1 & 63 | 128; store(bufOff, b1 << 8 | b0); bufOff += 2; } else { if ((c1 & 0xFC00) == 0xD800 && str + 2 < strEnd) { let c2 = load(str, 2); if ((c2 & 0xFC00) == 0xDC00) { c1 = 0x10000 + ((c1 & 0x03FF) << 10) | (c2 & 0x03FF); let b0 = c1 >> 18 | 240; let b1 = c1 >> 12 & 63 | 128; let b2 = c1 >> 6 & 63 | 128; let b3 = c1 & 63 | 128; store(bufOff, b3 << 24 | b2 << 16 | b1 << 8 | b0); bufOff += 4; str += 4; continue; } } let b0 = c1 >> 12 | 224; let b1 = c1 >> 6 & 63 | 128; let b2 = c1 & 63 | 128; store(bufOff, b1 << 8 | b0); store(bufOff, b2, 2); bufOff += 3; } str += 2; } if (nullTerminated) { store(bufOff++, 0); } return bufOff - buf; } export function decode(buf: ArrayBuffer, nullTerminated: bool = false): String { return decodeUnsafe(changetype(buf), buf.byteLength, nullTerminated); } // @ts-ignore: decorator @unsafe export function decodeUnsafe(buf: usize, len: usize, nullTerminated: bool = false): String { var bufOff = buf; var bufEnd = buf + len; assert(bufEnd >= bufOff); // guard wraparound var str = __alloc(len << 1, idof()); // max is one u16 char per u8 byte var strOff = str; while (bufOff < bufEnd) { let u0 = load(bufOff); ++bufOff; if (!(u0 & 128)) { // @ts-ignore: cast if (nullTerminated & !u0) break; store(strOff, u0); } else { if (bufEnd == bufOff) break; let u1 = load(bufOff) & 63; ++bufOff; if ((u0 & 224) == 192) { store(strOff, (u0 & 31) << 6 | u1); } else { if (bufEnd == bufOff) break; let u2 = load(bufOff) & 63; ++bufOff; if ((u0 & 240) == 224) { u0 = (u0 & 15) << 12 | u1 << 6 | u2; } else { if (bufEnd == bufOff) break; u0 = (u0 & 7) << 18 | u1 << 12 | u2 << 6 | load(bufOff) & 63; ++bufOff; } if (u0 < 0x10000) { store(strOff, u0); } else { u0 -= 0x10000; let lo = u0 >> 10 | 0xD800; let hi = (u0 & 0x03FF) | 0xDC00; store(strOff, lo | (hi << 16)); strOff += 2; } } } strOff += 2; } return changetype(__realloc(str, strOff - str)); // retains } } export namespace UTF16 { export function byteLength(str: string): i32 { return changetype(changetype(str) - BLOCK_OVERHEAD).rtSize; } export function encode(str: string): ArrayBuffer { var buf = __alloc(byteLength(str), idof()); encodeUnsafe(changetype(str), str.length, buf); return changetype(buf); // retains } // @ts-ignore: decorator @unsafe export function encodeUnsafe(str: usize, len: i32, buf: usize): usize { var size = len << 1; memory.copy(buf, changetype(str), size); return size; } export function decode(buf: ArrayBuffer): String { return decodeUnsafe(changetype(buf), buf.byteLength); } // @ts-ignore: decorator @unsafe export function decodeUnsafe(buf: usize, len: usize): String { var str = __alloc(len &= ~1, idof()); memory.copy(str, buf, len); return changetype(str); // retains } } }