diff --git a/.size-limit.json b/.size-limit.json
index a014aba9..3876dd09 100644
--- a/.size-limit.json
+++ b/.size-limit.json
@@ -2,42 +2,42 @@
{
"name": "es5-full",
"path": "lib/dist/es5/mod/ts-utils.js",
- "limit": "22.5 kb",
+ "limit": "24.5 kb",
"brotli": false,
"running": false
},
{
"name": "es6-full",
"path": "lib/dist/es6/mod/ts-utils.js",
- "limit": "21.5 kb",
+ "limit": "23.5 kb",
"brotli": false,
"running": false
},
{
"name": "es5-full-brotli",
"path": "lib/dist/es5/mod/ts-utils.js",
- "limit": "8.5 kb",
+ "limit": "9 kb",
"brotli": true,
"running": false
},
{
"name": "es6-full-brotli",
"path": "lib/dist/es6/mod/ts-utils.js",
- "limit": "8 kb",
+ "limit": "9 kb",
"brotli": true,
"running": false
},
{
"name": "es5-zip",
"path": "lib/dist/es5/mod/ts-utils.js",
- "limit": "9 Kb",
+ "limit": "9.5 Kb",
"gzip": true,
"running": false
},
{
"name": "es6-zip",
"path": "lib/dist/es6/mod/ts-utils.js",
- "limit": "9 Kb",
+ "limit": "9.5 Kb",
"gzip": true,
"running": false
},
diff --git a/README.md b/README.md
index 52d17cc2..b4af81f9 100644
--- a/README.md
+++ b/README.md
@@ -58,10 +58,13 @@ npm install @nevware21/ts-utils --save
- Deep copy/extend
- Object transformation helpers
-### String Manipulation
+### String Manipulation & Encoding
- Case conversion (camelCase, kebab-case, snake_case)
- String transformation utilities
-- HTML and JSON encoding
+- Data encoding: Base64 (standard and URL-safe), Hexadecimal, URI
+- Data decoding with ES5-compatible polyfill support
+- HTML and JSON encoding for security
+- Cross-environment compatibility with optimized implementations
### Function Helpers
- Function binding and proxying utilities
@@ -123,7 +126,7 @@ Below is a categorized list of all available utilities with direct links to thei
| String | [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getLength](https://nevware21.github.io/ts-utils/typedoc/functions/getLength.html)(); [isString](https://nevware21.github.io/ts-utils/typedoc/functions/isString.html)(); [strEndsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strEndsWith.html)(); [strIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strIndexOf.html)(); [strIsNullOrEmpty](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrEmpty.html)(); [strIsNullOrWhiteSpace](https://nevware21.github.io/ts-utils/typedoc/functions/strIsNullOrWhiteSpace.html)(); [strLastIndexOf](https://nevware21.github.io/ts-utils/typedoc/functions/strLastIndexOf.html)(); [strLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strLeft.html)(); [strPadEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strPadEnd.html)(); [strPadStart](https://nevware21.github.io/ts-utils/typedoc/functions/strPadStart.html)(); [strRepeat](https://nevware21.github.io/ts-utils/typedoc/functions/strRepeat.html)(); [strRight](https://nevware21.github.io/ts-utils/typedoc/functions/strRight.html)(); [strSlice](https://nevware21.github.io/ts-utils/typedoc/functions/strSlice.html)(); [strSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSplit.html)(); [strStartsWith](https://nevware21.github.io/ts-utils/typedoc/functions/strStartsWith.html)(); [strSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstr.html)(); [strSubstring](https://nevware21.github.io/ts-utils/typedoc/functions/strSubstring.html)(); [strSymSplit](https://nevware21.github.io/ts-utils/typedoc/functions/strSymSplit.html)(); [strTrim](https://nevware21.github.io/ts-utils/typedoc/functions/strTrim.html)(); [strTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimEnd.html)(); [strTrimLeft](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimLeft.html)(); [strTrimRight](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimRight.html)(); [strTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/strTrimStart.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)(); [strContains](https://nevware21.github.io/ts-utils/typedoc/functions/strContains.html)(); [strIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/strIncludes.html)();
[polyStrSubstr](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrSubstr.html)(); [polyStrTrim](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrim.html)(); [polyStrTrimEnd](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimEnd.html)(); [polyStrTrimStart](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrTrimStart.html)(); [polyStrIncludes](https://nevware21.github.io/ts-utils/typedoc/functions/polyStrIncludes.html)();
| Symbol | [WellKnownSymbols](https://nevware21.github.io/ts-utils/typedoc/enums/WellKnownSymbols.html) (const enum);
[getKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getKnownSymbol.html)(); [getSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/getSymbol.html)(); [hasSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/hasSymbol.html)(); [isSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/isSymbol.html)(); [newSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/newSymbol.html)(); [symbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolFor.html)(); [symbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/symbolKeyFor.html)();
[polyGetKnownSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyGetKnownSymbol.html)(); [polyNewSymbol](https://nevware21.github.io/ts-utils/typedoc/functions/polyNewSymbol.html)(); [polySymbolFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolFor.html)(); [polySymbolKeyFor](https://nevware21.github.io/ts-utils/typedoc/functions/polySymbolKeyFor.html)();
Polyfills are used to automatically backfill runtimes that do not support `Symbol`, not all of the Symbol functionality is provided.
| Timer | [createTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeout.html)(); [createTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/createTimeoutWith.html)(); [elapsedTime](https://nevware21.github.io/ts-utils/typedoc/functions/elapsedTime.html)(); [perfNow](https://nevware21.github.io/ts-utils/typedoc/functions/perfNow.html)(); [setGlobalTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setGlobalTimeoutOverrides.html)(); [setTimeoutOverrides](https://nevware21.github.io/ts-utils/typedoc/functions/setTimeoutOverrides.html)(); [utcNow](https://nevware21.github.io/ts-utils/typedoc/functions/utcNow.html)(); [scheduleIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleIdleCallback.html)(); [scheduleInterval](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleInterval.html)(); [scheduleTimeout](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeout.html)(); [scheduleTimeoutWith](https://nevware21.github.io/ts-utils/typedoc/functions/scheduleTimeoutWith.html)(); [hasIdleCallback](https://nevware21.github.io/ts-utils/typedoc/functions/hasIdleCallback.html)();
For runtimes that don't support `requestIdleCallback` normal setTimeout() is used with the values from [`setDefaultIdleTimeout`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultIdleTimeout.html)() and [`setDefaultMaxExecutionTime`](https://nevware21.github.io/ts-utils/typedoc/functions/setDefaultMaxExecutionTime.html)();
[polyUtcNow](https://nevware21.github.io/ts-utils/typedoc/functions/polyUtcNow.html)();
-| Conversion | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)();
+| Conversion & Encoding | [encodeAsJson](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsJson.html)(); [encodeAsHtml](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHtml.html)(); [encodeAsBase64](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64.html)(); [decodeBase64](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64.html)(); [encodeAsBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsBase64Url.html)(); [decodeBase64Url](https://nevware21.github.io/ts-utils/typedoc/functions/decodeBase64Url.html)(); [encodeAsHex](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsHex.html)(); [decodeHex](https://nevware21.github.io/ts-utils/typedoc/functions/decodeHex.html)(); [encodeAsUri](https://nevware21.github.io/ts-utils/typedoc/functions/encodeAsUri.html)(); [decodeUri](https://nevware21.github.io/ts-utils/typedoc/functions/decodeUri.html)(); [asString](https://nevware21.github.io/ts-utils/typedoc/functions/asString.html)(); [getIntValue](https://nevware21.github.io/ts-utils/typedoc/functions/getIntValue.html)(); [normalizeJsName](https://nevware21.github.io/ts-utils/typedoc/functions/normalizeJsName.html)(); [strLetterCase](https://nevware21.github.io/ts-utils/typedoc/functions/strLetterCase.html)(); [strCamelCase](https://nevware21.github.io/ts-utils/typedoc/functions/strCamelCase.html)(); [strKebabCase](https://nevware21.github.io/ts-utils/typedoc/functions/strKebabCase.html)(); [strSnakeCase](https://nevware21.github.io/ts-utils/typedoc/functions/strSnakeCase.html)(); [strUpper](https://nevware21.github.io/ts-utils/typedoc/functions/strUpper.html)(); [strLower](https://nevware21.github.io/ts-utils/typedoc/functions/strLower.html)();
| Cache | [createCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createCachedValue.html)(); [createDeferredCachedValue](https://nevware21.github.io/ts-utils/typedoc/functions/createDeferredCachedValue.html)(); [getDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getDeferred.html)(); [getWritableDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableDeferred.html)();
| Lazy | [getLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getLazy.html)(); [getWritableLazy](https://nevware21.github.io/ts-utils/typedoc/functions/getWritableLazy.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [setBypassLazyCache](https://nevware21.github.io/ts-utils/typedoc/functions/setBypassLazyCache.html)();
| Safe | [safe](https://nevware21.github.io/ts-utils/typedoc/functions/safe.html)(); [safeGetLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetLazy.html)(); [safeGet](https://nevware21.github.io/ts-utils/typedoc/functions/safeGet.html)(); [safeGetDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetDeferred.html)(); [safeGetWritableDeferred](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetWritableDeferred.html)(); [lazySafeGetInst](https://nevware21.github.io/ts-utils/typedoc/functions/lazySafeGetInst.html)(); [safeGetWritableLazy](https://nevware21.github.io/ts-utils/typedoc/functions/safeGetWritableLazy.html)();
diff --git a/lib/src/helpers/customError.ts b/lib/src/helpers/customError.ts
index 9e710c29..b9ef350f 100644
--- a/lib/src/helpers/customError.ts
+++ b/lib/src/helpers/customError.ts
@@ -55,7 +55,7 @@ function _setName(baseClass: any, name: string) {
* @param constructCb - [Optional] An optional callback function to call when a
* new Custom Error instance is being created.
* @param errorBase - [Optional] (since v0.9.6) The error class to extend for this class, defaults to Error.
- * @param superArgsFn - [Optional] (since v0.12.7) An optional function that receives the constructor arguments and
+ * @param superArgsFn - [Optional] (since v0.13.0) An optional function that receives the constructor arguments and
* returns the arguments to pass to the base class constructor. When not provided all constructor
* arguments are forwarded to the base class. Use this to support a different argument order or
* to pass a subset of arguments to the base class (similar to calling `super(...)` in a class).
diff --git a/lib/src/helpers/encode.ts b/lib/src/helpers/encode.ts
index 2487146f..5b309336 100644
--- a/lib/src/helpers/encode.ts
+++ b/lib/src/helpers/encode.ts
@@ -6,17 +6,26 @@
* Licensed under the MIT license.
*/
-import { NULL_VALUE, TO_STRING, UNDEF_VALUE } from "../internal/constants";
+import { EMPTY, NULL_VALUE, TO_STRING, UNDEF_VALUE } from "../internal/constants";
import { asString } from "../string/as_string";
import { strCamelCase } from "../string/conversion";
import { strPadStart } from "../string/pad";
+import { strRepeat } from "../string/repeat";
+import { strSubstr } from "../string/substring";
import { strUpper } from "../string/upper_lower";
-import { isNumber, isString, isUndefined } from "./base";
+import { isNumber, isStrictNullOrUndefined, isString, isUndefined } from "./base";
+import { ICachedValue } from "./cache";
+import { safeGetDeferred } from "./safe_lazy";
import { dumpObj } from "./diagnostics";
const DBL_QUOTE = "\"";
const INVALID_JS_NAME = /([^\w\d_$])/g;
+const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+const HEX_CHARS = "0123456789abcdef";
+let _btoa: ICachedValue<(value: string) => string>;
+let _atob: ICachedValue<(value: string) => string>;
let _htmlEntityCache: { [key: string]: string};
+let _base64Cache: { [key: string]: number };
/**
* Validates that the string name conforms to the JS IdentifierName specification and if not
@@ -177,3 +186,334 @@ export function encodeAsHtml(value: string) {
return asString(value).replace(/[&<>"']/g, match => "&" + _htmlEntityCache[match] + ";");
}
+
+/**
+ * Encode a string value as Base64
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The string value to encode
+ * @returns The Base64 encoded string
+ * @example
+ * ```ts
+ * encodeAsBase64("Hello"); // "SGVsbG8="
+ * encodeAsBase64("Hello World"); // "SGVsbG8gV29ybGQ="
+ * encodeAsBase64(""); // ""
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function encodeAsBase64(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ result = asString(value);
+
+ try {
+ // Use native btoa if available
+ !_btoa && (_btoa = safeGetDeferred(() => !isUndefined(btoa) ? btoa : _encodeBase64Polyfill, _encodeBase64Polyfill));
+ result = _btoa.v(result);
+ } catch (e) {
+ // Use polyfill on error
+ result = _encodeBase64Polyfill(result);
+ }
+ }
+
+ return result || EMPTY;
+}
+
+/**
+ * Decode a Base64 encoded string
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The Base64 encoded string to decode
+ * @returns The decoded string
+ * @example
+ * ```ts
+ * decodeBase64("SGVsbG8="); // "Hello"
+ * decodeBase64("SGVsbG8gV29ybGQ="); // "Hello World"
+ * decodeBase64(""); // ""
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function decodeBase64(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ result = asString(value);
+
+ try {
+ // Use native atob if available
+ !_atob && (_atob = safeGetDeferred(() => !isUndefined(atob) ? atob : _decodeBase64Polyfill, _decodeBase64Polyfill));
+ result = _atob.v(result);
+ } catch (e) {
+ // Use polyfill on error
+ result = _decodeBase64Polyfill(result);
+ }
+ }
+
+ return result || value || EMPTY;
+}
+
+/**
+ * Encode a string as URL-safe Base64
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The string value to encode
+ * @returns The URL-safe Base64 encoded string (replaces + with -, / with _, removes padding)
+ * @example
+ * ```ts
+ * encodeAsBase64Url("Hello"); // "SGVsbG8"
+ * encodeAsBase64Url("+++///"); // "LSsvLw"
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function encodeAsBase64Url(value: string): string {
+ let encoded = encodeAsBase64(value);
+
+ if (encoded) {
+ encoded = encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+ }
+
+ return encoded || EMPTY;
+}
+
+/**
+ * Decode a URL-safe Base64 encoded string
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The URL-safe Base64 encoded string to decode
+ * @returns The decoded string
+ * @example
+ * ```ts
+ * decodeBase64Url("SGVsbG8"); // "Hello"
+ * decodeBase64Url("LSsvLw"); // "+++///"
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function decodeBase64Url(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ result = asString(value);
+ let pad = 4 - (result.length % 4); // Add padding back
+
+ if (pad && pad !== 4) {
+ result = result + strRepeat("=", pad);
+ }
+
+ result = decodeBase64(result.replace(/-/g, "+").replace(/_/g, "/")) || EMPTY;
+ }
+
+ return result || value || EMPTY;
+}
+
+/**
+ * Encode a string as hexadecimal
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The string value to encode
+ * @returns The hexadecimal encoded string
+ * @example
+ * ```ts
+ * encodeAsHex("Hello"); // "48656c6c6f"
+ * encodeAsHex("A"); // "41"
+ * encodeAsHex(""); // ""
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function encodeAsHex(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ result = asString(value);
+
+ let hex = [];
+ for (let idx = 0; idx < result.length; idx++) {
+ let code = result.charCodeAt(idx);
+
+ hex.push(HEX_CHARS[(code >> 4) & 0xf]);
+ hex.push(HEX_CHARS[code & 0xf]);
+ }
+
+ result = hex.join("");
+ }
+
+ return result || value || EMPTY;
+}
+
+/**
+ * Decode a hexadecimal encoded string
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The hexadecimal encoded string to decode
+ * @returns The decoded string
+ * @example
+ * ```ts
+ * decodeHex("48656c6c6f"); // "Hello"
+ * decodeHex("41"); // "A"
+ * decodeHex(""); // ""
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function decodeHex(value: string): string {
+ let result = [];
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ let theValue = asString(value);
+
+ for (let idx = 0; idx < theValue.length; idx += 2) {
+ result.push(String.fromCharCode(parseInt(strSubstr(theValue, idx, 2), 16)));
+ }
+ }
+
+ return result.join("");
+}
+
+/**
+ * Encode a string using URI encoding
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The string value to encode
+ * @returns The URI encoded string
+ * @example
+ * ```ts
+ * encodeAsUri("Hello World"); // "Hello%20World"
+ * encodeAsUri("a+b=c"); // "a%2Bb%3Dc"
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function encodeAsUri(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ try {
+ result = encodeURIComponent(asString(value));
+ } catch (e) {
+ // Use original string on error
+ }
+ }
+
+ return result || value || EMPTY;
+}
+
+/**
+ * Decode a URI encoded string
+ * @since 0.13.0
+ * @group Encoding
+ * @group Conversion
+ * @param value - The URI encoded string to decode
+ * @returns The decoded string
+ * @example
+ * ```ts
+ * decodeUri("Hello%20World"); // "Hello World"
+ * decodeUri("a%2Bb%3Dc"); // "a+b=c"
+ * ```
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function decodeUri(value: string): string {
+ let result: string;
+
+ if (value || !isStrictNullOrUndefined(value)) {
+ try {
+ result = decodeURIComponent(asString(value));
+ } catch (e) {
+ // Use original string on error
+ }
+ }
+
+ return result || value || EMPTY;
+}
+
+/**
+ * Internal Base64 encoding polyfill for ES5 environments without btoa
+ * @internal
+ * @ignore
+ */
+export function _encodeBase64Polyfill(str: string): string {
+ let result = [];
+
+ if (str || !isStrictNullOrUndefined(str)) {
+ str = asString(str);
+
+ let len = str.length;
+ let lp = 0;
+
+ while (lp < len) {
+ let a = str.charCodeAt(lp++);
+
+ let hasB = lp < len;
+ let b = hasB ? str.charCodeAt(lp++) : 0;
+
+ let hasC = lp < len;
+ let c = hasC ? str.charCodeAt(lp++) : 0;
+
+ let bitmap = (a << 16) | (b << 8) | c;
+
+ result.push(BASE64_CHARS[(bitmap >> 18) & 63]);
+ result.push(BASE64_CHARS[(bitmap >> 12) & 63]);
+
+ if (hasB) {
+ result.push(BASE64_CHARS[(bitmap >> 6) & 63]);
+ } else {
+ result.push("=");
+ }
+
+ if (hasC) {
+ result.push(BASE64_CHARS[bitmap & 63]);
+ } else {
+ result.push("=");
+ }
+ }
+ }
+
+ return result.join("");
+}
+
+/**
+ * Internal Base64 decoding polyfill for ES5 environments without atob
+ * @internal
+ * @ignore
+ */
+export function _decodeBase64Polyfill(str: string): string {
+ let result = [];
+
+ if (str || !isStrictNullOrUndefined(str)) {
+ str = asString(str);
+ let len = str.length;
+
+ // Build the Base64 character cache if it doesn't exist
+ !_base64Cache && (_base64Cache = {});
+ if (!_base64Cache["A"]) {
+ for (let i = 0; i < BASE64_CHARS.length; i++) {
+ _base64Cache[BASE64_CHARS[i]] = i;
+ }
+ }
+
+ let idx = 0;
+ while (idx < len) {
+ let a = _base64Cache[str[idx++]] || 0;
+ let b = _base64Cache[str[idx++]] || 0;
+ let c = _base64Cache[str[idx++]] || 0;
+ let d = _base64Cache[str[idx++]] || 0;
+ let bitmap = (a << 18) | (b << 12) | (c << 6) | d;
+
+ result.push(String.fromCharCode((bitmap >> 16) & 255));
+
+ if (str[idx - 2] !== "=") {
+ result.push(String.fromCharCode((bitmap >> 8) & 255));
+ }
+
+ if (str[idx - 1] !== "=") {
+ result.push(String.fromCharCode(bitmap & 255));
+ }
+ }
+ }
+
+ return result.join("");
+}
diff --git a/lib/src/index.ts b/lib/src/index.ts
index 3c0dcdf1..0c5fb18e 100644
--- a/lib/src/index.ts
+++ b/lib/src/index.ts
@@ -41,7 +41,10 @@ export {
getGlobal, getInst, lazySafeGetInst, hasDocument, getDocument, hasWindow, getWindow, hasNavigator, getNavigator, hasHistory,
getHistory, isNode, isWebWorker
} from "./helpers/environment";
-export { encodeAsHtml, encodeAsJson, normalizeJsName } from "./helpers/encode";
+export {
+ encodeAsHtml, encodeAsJson, normalizeJsName, encodeAsBase64, decodeBase64, encodeAsBase64Url,
+ decodeBase64Url, encodeAsHex, decodeHex, encodeAsUri, decodeUri
+} from "./helpers/encode";
export { deepExtend, objExtend } from "./helpers/extend";
export { getValueByKey, setValueByKey, getValueByIter, setValueByIter } from "./helpers/get_set_value";
export { ILazyValue, getLazy, setBypassLazyCache, getWritableLazy } from "./helpers/lazy";
diff --git a/lib/test/bundle-size-check.js b/lib/test/bundle-size-check.js
index 1ea01258..215987f0 100644
--- a/lib/test/bundle-size-check.js
+++ b/lib/test/bundle-size-check.js
@@ -7,25 +7,25 @@ const configs = [
{
name: "es5-min-full",
path: "../bundle/es5/umd/ts-utils.min.js",
- limit: 25.5 * 1024, // 25.5 kb in bytes
+ limit: 27.5 * 1024, // 27.5 kb in bytes
compress: false
},
{
name: "es6-min-full",
path: "../bundle/es6/umd/ts-utils.min.js",
- limit: 25 * 1024, // 25 kb in bytes
+ limit: 27 * 1024, // 27 kb in bytes
compress: false
},
{
name: "es5-min-zip",
path: "../bundle/es5/umd/ts-utils.min.js",
- limit: 10.5 * 1024, // 10.5 kb in bytes
+ limit: 11 * 1024, // 11 kb in bytes
compress: true
},
{
name: "es6-min-zip",
path: "../bundle/es6/umd/ts-utils.min.js",
- limit: 10.5 * 1024, // 10.5 kb in bytes
+ limit: 11 * 1024, // 11 kb in bytes
compress: true
},
{
diff --git a/lib/test/src/common/helpers/encode.test.ts b/lib/test/src/common/helpers/encode.test.ts
index 68f56aad..4977e37c 100644
--- a/lib/test/src/common/helpers/encode.test.ts
+++ b/lib/test/src/common/helpers/encode.test.ts
@@ -7,8 +7,13 @@
*/
import { assert } from "@nevware21/tripwire-chai";
-import { encodeAsHtml, encodeAsJson, normalizeJsName } from "../../../../src/helpers/encode";
+import {
+ encodeAsHtml, encodeAsJson, normalizeJsName, encodeAsBase64, decodeBase64, encodeAsBase64Url,
+ decodeBase64Url, encodeAsHex, decodeHex, encodeAsUri, decodeUri, _encodeBase64Polyfill,
+ _decodeBase64Polyfill
+} from "../../../../src/helpers/encode";
import { arrContains } from "../../../../src/array/includes";
+import { strRepeat } from "../../../../src/string/repeat";
describe("encodeAsJson helper", () => {
it("null/undefined", () => {
@@ -153,4 +158,400 @@ describe("encodeAsHtml", () => {
it("Basic html encoding", () => {
assert.equal(encodeAsHtml(""), "<script src="javascript:alert('Hello');"></script>");
});
+});
+
+describe("encodeAsBase64 / decodeBase64", () => {
+ it("null / undefined / empty string", () => {
+ assert.equal(encodeAsBase64(null as any), "");
+ assert.equal(encodeAsBase64(undefined as any), "");
+ assert.equal(encodeAsBase64(""), "");
+ assert.equal(decodeBase64(""), "");
+ });
+
+ it("basic string encoding/decoding", () => {
+ assert.equal(encodeAsBase64("Hello"), "SGVsbG8=");
+ assert.equal(decodeBase64("SGVsbG8="), "Hello");
+
+ assert.equal(encodeAsBase64("Hello World"), "SGVsbG8gV29ybGQ=");
+ assert.equal(decodeBase64("SGVsbG8gV29ybGQ="), "Hello World");
+
+ assert.equal(encodeAsBase64("A"), "QQ==");
+ assert.equal(decodeBase64("QQ=="), "A");
+
+ assert.equal(encodeAsBase64("AB"), "QUI=");
+ assert.equal(decodeBase64("QUI="), "AB");
+
+ assert.equal(encodeAsBase64("ABC"), "QUJD");
+ assert.equal(decodeBase64("QUJD"), "ABC");
+ });
+
+ it("round-trip encoding/decoding", () => {
+ let values = ["", "a", "ab", "abc", "test123", "The quick brown fox", "~!@#$%^&*()"];
+ for (let i = 0; i < values.length; i++) {
+ let val = values[i];
+ assert.equal(decodeBase64(encodeAsBase64(val)), val, "Failed for: " + val);
+ }
+ });
+});
+
+describe("encodeAsBase64Url / decodeBase64Url", () => {
+ it("null / undefined / empty string", () => {
+ assert.equal(encodeAsBase64Url(null as any), "");
+ assert.equal(encodeAsBase64Url(undefined as any), "");
+ assert.equal(encodeAsBase64Url(""), "");
+ assert.equal(decodeBase64Url(""), "");
+ });
+
+ it("URL safe encoding/decoding", () => {
+ assert.equal(encodeAsBase64Url("Hello"), "SGVsbG8");
+ assert.equal(decodeBase64Url("SGVsbG8"), "Hello");
+
+ assert.equal(encodeAsBase64Url("Hello World"), "SGVsbG8gV29ybGQ");
+ assert.equal(decodeBase64Url("SGVsbG8gV29ybGQ"), "Hello World");
+ });
+
+ it("special character handling for URL safety", () => {
+ // Test strings that produce + or / in standard base64
+ let values = ["", "?>>", "test"];
+ for (let i = 0; i < values.length; i++) {
+ let val = values[i];
+ let encoded = encodeAsBase64Url(val);
+ // Should not contain +, /, or =
+ assert.ok(!encoded.includes("+"), "Should not contain + in: " + encoded);
+ assert.ok(!encoded.includes("/"), "Should not contain / in: " + encoded);
+ assert.ok(!encoded.includes("="), "Should not contain = in: " + encoded);
+ assert.equal(decodeBase64Url(encoded), val, "Failed round-trip for: " + val);
+ }
+ });
+});
+
+describe("encodeAsHex / decodeHex", () => {
+ it("null / undefined / empty string", () => {
+ assert.equal(encodeAsHex(null as any), "");
+ assert.equal(encodeAsHex(undefined as any), "");
+ assert.equal(encodeAsHex(""), "");
+ assert.equal(decodeHex(""), "");
+ });
+
+ it("basic hex encoding/decoding", () => {
+ assert.equal(encodeAsHex("A"), "41");
+ assert.equal(decodeHex("41"), "A");
+
+ assert.equal(encodeAsHex("Hello"), "48656c6c6f");
+ assert.equal(decodeHex("48656c6c6f"), "Hello");
+
+ assert.equal(encodeAsHex("test"), "74657374");
+ assert.equal(decodeHex("74657374"), "test");
+
+ assert.equal(encodeAsHex("0"), "30");
+ assert.equal(decodeHex("30"), "0");
+ });
+
+ it("round-trip hex encoding/decoding", () => {
+ let values = ["", "a", "Hello", "Test123", "The quick brown fox"];
+ for (let i = 0; i < values.length; i++) {
+ let val = values[i];
+ assert.equal(decodeHex(encodeAsHex(val)), val, "Failed for: " + val);
+ }
+ });
+
+ it("special characters in hex", () => {
+ assert.equal(encodeAsHex("!@#"), "214023");
+ });
+});
+
+describe("encodeAsUri / decodeUri", () => {
+ it("null / undefined / empty string", () => {
+ assert.equal(encodeAsUri(null as any), "");
+ assert.equal(encodeAsUri(undefined as any), "");
+ assert.equal(encodeAsUri(""), "");
+ assert.equal(decodeUri(""), "");
+ });
+
+ it("basic URI encoding/decoding", () => {
+ assert.equal(encodeAsUri("Hello"), "Hello");
+ assert.equal(decodeUri("Hello"), "Hello");
+
+ assert.equal(encodeAsUri("Hello World"), "Hello%20World");
+ assert.equal(decodeUri("Hello%20World"), "Hello World");
+
+ assert.equal(encodeAsUri("a+b=c"), "a%2Bb%3Dc");
+ assert.equal(decodeUri("a%2Bb%3Dc"), "a+b=c");
+ });
+
+ it("special URI characters", () => {
+ assert.ok(encodeAsUri("?") !== "?");
+ assert.equal(decodeUri(encodeAsUri("?")), "?");
+
+ assert.ok(encodeAsUri("&") !== "&");
+ assert.equal(decodeUri(encodeAsUri("&")), "&");
+
+ assert.ok(encodeAsUri("#") !== "#");
+ assert.equal(decodeUri(encodeAsUri("#")), "#");
+ });
+
+ it("round-trip URI encoding/decoding", () => {
+ let values = ["", "test", "hello world", "a=b&c=d", "test?query=1"];
+ for (let i = 0; i < values.length; i++) {
+ let val = values[i];
+ assert.equal(decodeUri(encodeAsUri(val)), val, "Failed for: " + val);
+ }
+ });
+});
+
+describe("Base64 Polyfill Coverage - Iteration & Padding", () => {
+ it("various input lengths to exercise polyfill iteration", () => {
+ // Test different lengths to ensure all code paths are hit
+ for (let len = 1; len <= 30; len++) {
+ let str = "";
+ for (let i = 0; i < len; i++) {
+ str += String.fromCharCode((i % 26) + 65); // A-Z cycle
+ }
+ let encoded = encodeAsBase64(str);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, str, "Round-trip failed for length: " + len);
+ }
+ });
+
+ it("padding scenarios 1 byte (1 mod 3)", () => {
+ // Lengths: 1, 4, 7, 10... should have 2 padding chars
+ assert.equal(decodeBase64(encodeAsBase64("A")), "A");
+ assert.equal(decodeBase64(encodeAsBase64("ABCD")), "ABCD");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFG")), "ABCDEFG");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJ")), "ABCDEFGHIJ");
+ });
+
+ it("padding scenarios 2 bytes (2 mod 3)", () => {
+ // Lengths: 2, 5, 8, 11... should have 1 padding char
+ assert.equal(decodeBase64(encodeAsBase64("AB")), "AB");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDE")), "ABCDE");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFGH")), "ABCDEFGH");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJK")), "ABCDEFGHIJK");
+ });
+
+ it("padding scenarios 0 bytes (0 mod 3)", () => {
+ // Lengths: 3, 6, 9, 12... should have no padding
+ assert.equal(decodeBase64(encodeAsBase64("ABC")), "ABC");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEF")), "ABCDEF");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHI")), "ABCDEFGHI");
+ assert.equal(decodeBase64(encodeAsBase64("ABCDEFGHIJKL")), "ABCDEFGHIJKL");
+ });
+
+ it("all ASCII characters", () => {
+ // Test ASCII range to ensure polyfill handles all byte values
+ let ascii = "";
+ for (let i = 32; i < 127; i++) {
+ ascii += String.fromCharCode(i);
+ }
+ let encoded = encodeAsBase64(ascii);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, ascii, "Failed for ASCII 32-126");
+ });
+
+ it("special byte values across full range", () => {
+ // Test with various byte values (0-255 range)
+ let binaryStr = "";
+ for (let i = 0; i < 256; i++) {
+ binaryStr += String.fromCharCode(i);
+ }
+ let encoded = encodeAsBase64(binaryStr);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, binaryStr, "Failed for full byte range 0-255");
+ });
+
+ it("large input to validate iteration logic", () => {
+ // Test with larger inputs - 3000 bytes
+ let large = "";
+ for (let i = 0; i < 3000; i++) {
+ large += String.fromCharCode((i % 256));
+ }
+ let encoded = encodeAsBase64(large);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, large, "Failed for large 3000-byte input");
+ });
+
+ it("repeated characters at different lengths", () => {
+ // Ensures polyfill iteration works with repetitive data
+ for (let len = 1; len <= 50; len++) {
+ let str = strRepeat("X", len);
+ assert.equal(decodeBase64(encodeAsBase64(str)), str, "Failed for " + len + " X's");
+ }
+ });
+
+ it("polyfill cache initialization persistence", () => {
+ // Verify cache works correctly across multiple calls
+ let values = ["hello", "world", "test123", "base64encoding"];
+ for (let i = 0; i < values.length; i++) {
+ let val = values[i];
+ let encoded = encodeAsBase64(val);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, val, "Failed for: " + val);
+ }
+ });
+
+ it("inputs generating base64 special chars", () => {
+ // Ensure proper handling of +, /, = in output
+ let testCases = [
+ "Q@@@@"
+ ];
+ for (let i = 0; i < testCases.length; i++) {
+ let val = testCases[i];
+ let encoded = encodeAsBase64(val);
+ let decoded = decodeBase64(encoded);
+ assert.equal(decoded, val, "Failed for: " + val);
+ }
+ });
+});
+
+describe("Base64 Polyfill Verification Against Native Implementation", () => {
+ // Check if native implementations are available
+ let hasNativeBase64 = typeof btoa !== "undefined" && typeof atob !== "undefined";
+
+ if (hasNativeBase64) {
+ it("_encodeBase64Polyfill matches native btoa encoding", () => {
+ let testStrings = [
+ "",
+ "A",
+ "AB",
+ "ABC",
+ "Hello",
+ "Hello World",
+ "The quick brown fox jumps over the lazy dog",
+ "test123!@#$%^&*()",
+ "0",
+ "01",
+ "012",
+ "0123456789"
+ ];
+
+ for (let i = 0; i < testStrings.length; i++) {
+ let str = testStrings[i];
+ let polyfillResult = _encodeBase64Polyfill(str);
+ let nativeResult = btoa(str);
+ assert.equal(polyfillResult, nativeResult, "Polyfill encode mismatch for: " + str);
+ }
+ });
+
+ it("_decodeBase64Polyfill matches native atob decoding", () => {
+ let testStrings = [
+ "",
+ "QQ==",
+ "QUI=",
+ "QUJD",
+ "SGVsbG8=",
+ "SGVsbG8gV29ybGQ=",
+ "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==",
+ "dGVzdDEyMyFAIyQlXiYqKCk="
+ ];
+
+ for (let i = 0; i < testStrings.length; i++) {
+ let str = testStrings[i];
+ let polyfillResult = _decodeBase64Polyfill(str);
+ let nativeResult = atob(str);
+ assert.equal(polyfillResult, nativeResult, "Polyfill decode mismatch for: " + str);
+ }
+ });
+
+ it("Polyfill encode/decode round-trip matches native", () => {
+ let testStrings = [
+ "",
+ "a",
+ "ab",
+ "abc",
+ "test",
+ "Hello World",
+ "The quick brown fox",
+ "!@#$%^&*()",
+ "0",
+ "01",
+ "012"
+ ];
+
+ for (let i = 0; i < testStrings.length; i++) {
+ let original = testStrings[i];
+
+ // Test polyfill consistency
+ let polyfillEncoded = _encodeBase64Polyfill(original);
+ let polyfillDecoded = _decodeBase64Polyfill(polyfillEncoded);
+ assert.equal(polyfillDecoded, original, "Polyfill round-trip failed for: " + original);
+
+ // Test native consistency
+ let nativeEncoded = btoa(original);
+ let nativeDecoded = atob(nativeEncoded);
+ assert.equal(nativeDecoded, original, "Native round-trip failed for: " + original);
+
+ // Test polyfill matches native
+ assert.equal(polyfillEncoded, nativeEncoded, "Polyfill/native encode mismatch for: " + original);
+ assert.equal(polyfillDecoded, nativeDecoded, "Polyfill/native decode mismatch for: " + original);
+ }
+ });
+
+ it("Polyfill handles all byte values 0-255", () => {
+ // Create string with all possible byte values
+ let allBytes = "";
+ for (let i = 0; i < 256; i++) {
+ allBytes += String.fromCharCode(i);
+ }
+
+ let polyfillEncoded = _encodeBase64Polyfill(allBytes);
+ let nativeEncoded = btoa(allBytes);
+ assert.equal(polyfillEncoded, nativeEncoded, "Polyfill/native mismatch for byte range 0-255");
+
+ let polyfillDecoded = _decodeBase64Polyfill(nativeEncoded);
+ assert.equal(polyfillDecoded, allBytes, "Polyfill cannot decode native output for byte range");
+ });
+
+ it("Polyfill handles various input lengths to test padding", () => {
+ // Test lengths 0-30 to ensure padding is correct
+ for (let len = 0; len <= 30; len++) {
+ let str = strRepeat("X", len);
+ let polyfillEncoded = _encodeBase64Polyfill(str);
+ let nativeEncoded = btoa(str);
+ assert.equal(polyfillEncoded, nativeEncoded, "Padding mismatch for length: " + len);
+ }
+ });
+
+ it("Polyfill handles large inputs consistently with native", () => {
+ // Test with a 10KB input
+ let large = "";
+ for (let i = 0; i < 10000; i++) {
+ large += String.fromCharCode(i % 256);
+ }
+
+ let polyfillEncoded = _encodeBase64Polyfill(large);
+ let nativeEncoded = btoa(large);
+ assert.equal(polyfillEncoded, nativeEncoded, "Large input encoding mismatch");
+
+ let polyfillDecoded = _decodeBase64Polyfill(polyfillEncoded);
+ assert.equal(polyfillDecoded, large, "Large input round-trip failed");
+ });
+
+ it("Polyfill decode works with both uppercase and standard characters", () => {
+ // Generate some base64 and verify polyfill can decode it
+ let testStrings = ["test", "hello world", "The quick brown fox"];
+
+ for (let i = 0; i < testStrings.length; i++) {
+ let original = testStrings[i];
+ let nativeEncoded = btoa(original);
+ let polyfillDecoded = _decodeBase64Polyfill(nativeEncoded);
+ assert.equal(polyfillDecoded, original, "Polyfill cannot decode native encoding for: " + original);
+ }
+ });
+
+ it("Polyfill encode produces output that native atob can decode", () => {
+ let testStrings = ["", "a", "ab", "abc", "Hello", "Test123!"];
+
+ for (let i = 0; i < testStrings.length; i++) {
+ let original = testStrings[i];
+ let polyfillEncoded = _encodeBase64Polyfill(original);
+ let nativeDecoded = atob(polyfillEncoded);
+ assert.equal(nativeDecoded, original, "Native atob cannot decode polyfill output for: " + original);
+ }
+ });
+ } else {
+ // Fallback if native implementations aren't available (very rare)
+ it("Native btoa/atob are available", () => {
+ assert.ok(hasNativeBase64, "Native btoa/atob should be available in this environment");
+ });
+ }
});
\ No newline at end of file