/**
* MIT License
*
* Copyright (c) 2025 Chris M. Perez
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Option, pipe, Predicate } from 'effect';
import {
type EffuseNode,
type EffuseChild,
type ElementProps,
type ElementNode,
type BlueprintNode,
type BlueprintDef,
type Portals,
type PortalFn,
CreateElementNode,
CreateBlueprintNode,
createFragmentNode,
createTextNode,
} from './node.js';
import { EFFUSE_NODE } from '../constants.js';
export function el(
tag: string,
props?: ElementProps | null,
...children: EffuseChild[]
): ElementNode;
export function el
(
blueprint: BlueprintDef
,
props?: P | null,
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
portals?: Portals | PortalFn | null
): BlueprintNode
;
export function el(
tagOrBlueprint: string | BlueprintDef,
propsOrNull?: ElementProps | Record | null,
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
...rest: (EffuseChild | Portals | PortalFn | null)[]
): EffuseNode {
if (Predicate.isString(tagOrBlueprint)) {
const props = propsOrNull as ElementProps | null;
const children = normalizeChildren(rest as EffuseChild[]);
return CreateElementNode({
[EFFUSE_NODE]: true,
tag: tagOrBlueprint,
props: props ?? null,
children,
key: pipe(
Option.fromNullable(props),
Option.flatMap((p) => Option.fromNullable(p.key)),
Option.getOrUndefined
),
});
}
const blueprint = tagOrBlueprint;
const props = propsOrNull ?? {};
const portalsArg = rest[0];
let portals: Portals | null = null;
if (Predicate.isFunction(portalsArg)) {
portals = { default: portalsArg as PortalFn };
} else if (
portalsArg &&
Predicate.isObject(portalsArg) &&
!Array.isArray(portalsArg)
) {
portals = portalsArg as Portals;
}
return CreateBlueprintNode({
[EFFUSE_NODE]: true,
blueprint,
props,
portals,
key: (props as { key?: string | number }).key,
});
}
// Build fragment node
export const fragment = (...children: EffuseChild[]): EffuseNode => {
return createFragmentNode(normalizeChildren(children));
};
const normalizeChildren = (children: EffuseChild[]): EffuseChild[] => {
const result: EffuseChild[] = [];
for (const child of children) {
if (Predicate.isNullable(child) || Predicate.isBoolean(child)) {
continue;
}
if (Array.isArray(child)) {
result.push(...normalizeChildren(child));
} else {
result.push(child);
}
}
return result;
};
// Convert child to reactive node
export const toNode = (child: EffuseChild): EffuseNode | null => {
if (Predicate.isNullable(child) || Predicate.isBoolean(child)) {
return null;
}
if (Predicate.isString(child)) {
return createTextNode(child);
}
if (Predicate.isNumber(child)) {
return createTextNode(String(child));
}
if (Predicate.isObject(child) && EFFUSE_NODE in child) {
return child;
}
return null;
};