Files
homarr/packages/custom-widgets/src/jsx/analyzer-language.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

139 lines
4.7 KiB
TypeScript

import { enabledCustomJsxComponents } from "../core/component-registry";
import type { AstNode } from "./analyzer-ast";
import { nodeOf, staticPropertyName } from "./analyzer-ast";
export const ROOT_BINDINGS = new Set([
"Array",
"Boolean",
"Date",
"Infinity",
"JSON",
"Math",
"NaN",
"Number",
"Object",
"String",
"data",
"options",
"inputs",
"status",
"decodeURIComponent",
"encodeURIComponent",
"isFinite",
"isNaN",
"parseFloat",
"parseInt",
"undefined",
]);
export const RESERVED_LOCAL_BINDINGS = new Set(["data", "status", "options", "inputs"]);
const callableRootBindings = new Set([
"Boolean",
"Number",
"String",
"decodeURIComponent",
"encodeURIComponent",
"isFinite",
"isNaN",
"parseFloat",
"parseInt",
]);
const callableRootMembers: Readonly<Record<string, ReadonlySet<string>>> = {
Array: new Set(["from", "isArray"]),
Date: new Set([
"create",
"getDay",
"getMonth",
"getTime",
"getYear",
"now",
"toISOString",
"toLocaleDateString",
"toLocaleString",
"toLocaleTimeString",
]),
JSON: new Set(["stringify"]),
Math: new Set(["abs", "ceil", "floor", "max", "min", "pow", "round", "sqrt"]),
Object: new Set(["entries", "keys", "values"]),
};
const enabledComponentNames = enabledCustomJsxComponents.map((component) => component.name);
export function closestCustomJsxComponentName(value: string): string | undefined {
let closest: { name: string; distance: number } | undefined;
for (const name of enabledComponentNames) {
const distance = editDistance(value.toLowerCase(), name.toLowerCase());
if (!closest || distance < closest.distance) closest = { name, distance };
}
return closest && closest.distance <= Math.max(2, Math.floor(value.length / 3)) ? closest.name : undefined;
}
export function customJsxTagName(node: AstNode): string | null {
if (node.type === "JSXIdentifier") return String(node.name ?? "");
if (node.type !== "JSXMemberExpression") return null;
const object = nodeOf(node.object);
const property = nodeOf(node.property);
const left = object ? customJsxTagName(object) : null;
const right = property ? customJsxTagName(property) : null;
return left && right ? `${left}.${right}` : null;
}
export function isSafeLiteralCustomJsxUrl(value: string): boolean {
if (value.startsWith("#") || (value.startsWith("/") && !value.startsWith("//"))) return true;
try {
const url = new URL(value);
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
} catch {
return false;
}
}
export function isStaticallyCallableBinding(node: AstNode): boolean {
if (node.type === "Identifier") return callableRootBindings.has(String(node.name));
if (node.type !== "MemberExpression") return false;
const object = nodeOf(node.object);
const property = nodeOf(node.property);
if (object?.type !== "Identifier") return false;
const propertyName = node.computed ? staticPropertyName(property) : String(property?.name ?? "");
return Boolean(propertyName && callableRootMembers[String(object.name)]?.has(propertyName));
}
export function canStaticallyProduceCallable(node: AstNode): boolean {
if (isStaticallyCallableBinding(node) || ["ArrowFunctionExpression", "FunctionExpression"].includes(node.type)) {
return true;
}
if (node.type === "ConditionalExpression") {
const consequent = nodeOf(node.consequent);
const alternate = nodeOf(node.alternate);
return Boolean(
(consequent && canStaticallyProduceCallable(consequent)) ||
(alternate && canStaticallyProduceCallable(alternate)),
);
}
if (node.type === "LogicalExpression") {
const left = nodeOf(node.left);
const right = nodeOf(node.right);
return Boolean((left && canStaticallyProduceCallable(left)) || (right && canStaticallyProduceCallable(right)));
}
if (node.type === "ChainExpression") {
const expression = nodeOf(node.expression);
return Boolean(expression && canStaticallyProduceCallable(expression));
}
return false;
}
function editDistance(left: string, right: string): number {
const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
const current = [leftIndex];
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
current[rightIndex] = Math.min(
(current[rightIndex - 1] ?? 0) + 1,
(previous[rightIndex] ?? 0) + 1,
(previous[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1),
);
}
previous.splice(0, previous.length, ...current);
}
return previous[right.length] ?? Math.max(left.length, right.length);
}