Files
homarr/packages/custom-widgets/src/jsx/collection-methods.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

206 lines
8.5 KiB
TypeScript

import { isSafeCallable } from "./safe-bindings";
import { isInterpreterCallback, normalizedSliceIndex, numericArgument, SafeJsxError } from "./interpreter-foundation";
import type { InterpreterCallback } from "./interpreter-foundation";
import type { InterpreterMethodContext } from "./interpreter-method-context";
import { callStringMethod } from "./string-methods";
export function callCollectionMethod(
receiver: unknown,
method: string,
args: unknown[],
depth: number,
context: InterpreterMethodContext,
): { handled: boolean; value?: unknown } {
if (Array.isArray(receiver)) {
if (method === "map" || method === "filter" || method === "flatMap") {
const callback = args[0];
if (isSafeCallable(callback)) {
const result: unknown[] = [];
for (let index = 0; index < receiver.length; index += 1) {
context.budget.collection();
const callbackResult = callback(receiver[index] as never);
if (method === "flatMap" && Array.isArray(callbackResult)) {
context.budget.collection(callbackResult.length);
result.push(...callbackResult);
} else if (method === "map" || method === "flatMap") result.push(callbackResult);
else if (callbackResult) result.push(receiver[index]);
}
return { handled: true, value: result };
}
if (!isInterpreterCallback(callback)) throw new SafeJsxError(`${method} requires an inline arrow callback`);
const result: unknown[] = [];
for (let index = 0; index < receiver.length; index += 1) {
context.budget.collection();
const callbackResult = context.runCallback(callback, [receiver[index], index, receiver], depth + 1);
if (method === "flatMap" && Array.isArray(callbackResult)) {
context.budget.collection(callbackResult.length);
result.push(...callbackResult);
} else if (method === "map" || method === "flatMap" || callbackResult) {
result.push(method === "map" || method === "flatMap" ? callbackResult : receiver[index]);
}
}
return { handled: true, value: result };
}
if (method === "concat") {
const result = [...receiver];
for (const value of args) {
if (Array.isArray(value)) {
context.budget.collection(value.length);
result.push(...value);
} else {
context.budget.collection();
result.push(value);
}
}
return { handled: true, value: result };
}
if (method === "pop") {
context.budget.collection();
return { handled: true, value: receiver.length > 0 ? receiver[receiver.length - 1] : undefined };
}
if (method === "slice") {
const start = normalizedSliceIndex(args[0], receiver.length, 0);
const end = normalizedSliceIndex(args[1], receiver.length, receiver.length);
const result: unknown[] = [];
for (let index = start; index < Math.max(start, end); index += 1) {
context.budget.collection();
result.push(receiver[index]);
}
return { handled: true, value: result };
}
if (method === "join") {
context.budget.collection(receiver.length);
const separator = String(args[0] ?? ",");
let result = "";
receiver.forEach((item, index) => {
result += `${index === 0 ? "" : separator}${String(item ?? "")}`;
context.budget.string(result);
});
return { handled: true, value: result };
}
if (method === "includes") {
context.budget.collection(receiver.length);
return { handled: true, value: receiver.some((item) => Object.is(item, args[0])) };
}
if (method === "indexOf") {
context.budget.collection(receiver.length);
return { handled: true, value: receiver.findIndex((item) => Object.is(item, args[0])) };
}
if (method === "find" || method === "findIndex" || method === "some" || method === "every") {
const callback = args[0];
const useSafe = isSafeCallable(callback);
if (!useSafe && !isInterpreterCallback(callback))
throw new SafeJsxError(`${method} requires an inline arrow callback`);
for (let index = 0; index < receiver.length; index += 1) {
context.budget.collection();
const result = useSafe
? (callback as (...a: never[]) => unknown)(receiver[index] as never)
: context.runCallback(callback as InterpreterCallback, [receiver[index], index, receiver], depth + 1);
if (method === "find" && result) return { handled: true, value: receiver[index] };
if (method === "findIndex" && result) return { handled: true, value: index };
if (method === "some" && result) return { handled: true, value: true };
if (method === "every" && !result) return { handled: true, value: false };
}
const defaults: Record<string, unknown> = { find: undefined, findIndex: -1, some: false, every: true };
return { handled: true, value: defaults[method] };
}
if (method === "sort") {
const callback = args[0];
if (!isInterpreterCallback(callback)) throw new SafeJsxError("sort requires an inline arrow callback");
const copy = [...receiver];
copy.sort((left, right) => {
context.budget.collection();
const delta = Number(context.runCallback(callback, [left, right], depth + 1));
if (!Number.isFinite(delta)) return 0;
return delta;
});
return { handled: true, value: copy };
}
if (method === "reduce") {
const callback = args[0];
if (!isInterpreterCallback(callback)) throw new SafeJsxError("reduce requires an inline arrow callback");
if (receiver.length === 0 && args.length < 2) {
throw new SafeJsxError("Reduce of empty array with no initial value");
}
let startIndex = 0;
let accumulator: unknown;
if (args.length < 2) {
accumulator = receiver[0];
startIndex = 1;
} else {
accumulator = args[1];
}
for (let index = startIndex; index < receiver.length; index += 1) {
context.budget.collection();
accumulator = context.runCallback(callback, [accumulator, receiver[index], index, receiver], depth + 1);
}
return { handled: true, value: accumulator };
}
if (method === "at") {
const index = numericArgument(args[0], 0);
let normalizedIndex = index;
if (index < 0) normalizedIndex = receiver.length + index;
context.budget.collection();
const outOfBounds = normalizedIndex < 0 || normalizedIndex >= receiver.length;
return { handled: true, value: outOfBounds ? undefined : receiver[normalizedIndex] };
}
if (method === "flat") {
// ponytail: only depth=1 supported; deeper nesting requires recursive budget tracking
const flatDepth = numericArgument(args[0], 1);
if (flatDepth !== 1) throw new SafeJsxError("flat() only supports depth 1");
const result: unknown[] = [];
for (const item of receiver) {
context.budget.collection();
if (Array.isArray(item)) {
for (const nested of item) {
context.budget.collection();
result.push(nested);
}
} else {
result.push(item);
}
}
return { handled: true, value: result };
}
if (method === "reverse") {
const copy = [...receiver];
const result: unknown[] = [];
for (let index = copy.length - 1; index >= 0; index -= 1) {
context.budget.collection();
result.push(copy[index]);
}
return { handled: true, value: result };
}
}
if (typeof receiver === "string") return callStringMethod(receiver, method, args, context);
if (receiver instanceof RegExp && method === "test") {
const value = String(args[0] ?? "");
return { handled: true, value: receiver.test(value) };
}
if (typeof receiver === "number") {
if (method === "toFixed") {
const digits = Math.max(0, Math.min(20, numericArgument(args[0], 0)));
return { handled: true, value: receiver.toFixed(digits) };
}
if (method === "toPrecision") {
const precision = Math.max(1, Math.min(21, numericArgument(args[0], 1)));
return { handled: true, value: receiver.toPrecision(precision) };
}
if (method === "toLocaleString") {
return { handled: true, value: context.budget.string(receiver.toLocaleString()) };
}
}
if (
(typeof receiver === "number" || typeof receiver === "boolean" || typeof receiver === "string") &&
method === "toString"
) {
return { handled: true, value: context.budget.string(String(receiver)) };
}
return { handled: false };
}