From 98789b438be327dbdad05d16af9d7844e76c758f Mon Sep 17 00:00:00 2001 From: natxocc Date: Thu, 9 Apr 2026 21:59:59 +0200 Subject: [PATCH] 1.2 1.3 --- sigpro2.js => sigpro v1.2.1.js | 10 +- sigpro v1.2.2.js | 545 +++++++++++++++++++++++++++++++++ sigpro v1.3.js | 343 +++++++++++++++++++++ sigpro.deep.js | 311 ------------------- sigpro_gemini.js | 261 ---------------- sigworkPro.js | 438 ++++++++++++++++++++++++++ 6 files changed, 1334 insertions(+), 574 deletions(-) rename sigpro2.js => sigpro v1.2.1.js (97%) create mode 100644 sigpro v1.2.2.js create mode 100644 sigpro v1.3.js delete mode 100644 sigpro.deep.js delete mode 100644 sigpro_gemini.js create mode 100644 sigworkPro.js diff --git a/sigpro2.js b/sigpro v1.2.1.js similarity index 97% rename from sigpro2.js rename to sigpro v1.2.1.js index 7258d1c..6ca129f 100644 --- a/sigpro2.js +++ b/sigpro v1.2.1.js @@ -1,5 +1,5 @@ /** - * SigPro v1.2.0 + * SigPro v1.2.1 */ const SigPro = (() => { const doc = typeof document !== "undefined" ? document : null; @@ -95,6 +95,7 @@ const SigPro = (() => { cache.set(obj, proxy); return proxy; }; + // Watch for changes const Watch = (target, cb) => { const explicit = isArr(target), runner = () => { if (runner._deleted) return; @@ -114,6 +115,7 @@ const SigPro = (() => { runner(); return runner.stop; }; + // Create element with props and children const Tag = (tag, props = {}, children = []) => { if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; } const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag); @@ -150,6 +152,7 @@ const SigPro = (() => { append(children); return el; }; + // Render a function to a container const Render = (fn) => { const cleanups = new Set(), prev = currentOwner, container = doc.createElement("div"); container.style.display = "contents"; currentOwner = { cleanups }; @@ -159,6 +162,7 @@ const SigPro = (() => { return { _isRuntime: true, container, destroy: () => { runCleanups(cleanups); cleanupNode(container); container.remove(); } }; }; + // Conditional rendering const If = (cond, t, f = null, trans = null) => { const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]); let view = null, last = null; @@ -177,6 +181,7 @@ const SigPro = (() => { return root; }; + // For loop const For = (src, itemFn, keyFn) => { const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]); let cache = new Map(); @@ -199,7 +204,7 @@ const SigPro = (() => { return root; }; - // --- ROUTER SYSTEM --- + // Router SPA hash const Router = (routes) => { const getHash = () => window.location.hash.slice(1) || "/"; const path = $(getHash()); @@ -248,6 +253,7 @@ const SigPro = (() => { return { $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onUnmount }; })(); +// AutoRegister DX in window, remove if don't want a dirty window if (typeof window !== "undefined") { Object.assign(window, SigPro); "div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg" diff --git a/sigpro v1.2.2.js b/sigpro v1.2.2.js new file mode 100644 index 0000000..7cbcd53 --- /dev/null +++ b/sigpro v1.2.2.js @@ -0,0 +1,545 @@ +/** + * SigPro v1.2.2 - ESM version with improved XSS blocking + */ + +// DOM reference +const documentReference = typeof document !== "undefined" ? document : null + +// Type checking helpers +const isArray = Array.isArray +const objectAssign = Object.assign +const isFunction = (value) => typeof value === "function" +const isObject = (value) => value && typeof value === "object" + +// Convert any value to a DOM node +const ensureNode = (nodeOrValue) => { + if (nodeOrValue && nodeOrValue._isRuntime) { + return nodeOrValue.container + } + if (nodeOrValue instanceof Node) { + return nodeOrValue + } + return documentReference.createTextNode(nodeOrValue ?? "") +} + +// --- REACTIVITY CORE --- +let currentActiveEffect = null +let currentOwnerEffect = null +let isFlushingEffects = false +const effectQueue = new Set() +const mountedNodesRegistry = new WeakMap() + +// Dispose an effect and its entire tree +const disposeEffect = (effect) => { + if (!effect || effect._disposed) { + return + } + effect._disposed = true + const stack = [effect] + while (stack.length) { + const currentEffect = stack.pop() + if (currentEffect._cleanups) { + for (const cleanupFunction of currentEffect._cleanups) { + cleanupFunction() + } + currentEffect._cleanups.clear() + } + if (currentEffect._children) { + for (const childEffect of currentEffect._children) { + stack.push(childEffect) + } + currentEffect._children.clear() + } + if (currentEffect._deps) { + for (const dependencySet of currentEffect._deps) { + dependencySet.delete(currentEffect) + } + currentEffect._deps.clear() + } + } +} + +// Create a reactive effect +const createReactiveEffect = (effectFunction, isComputed = false) => { + const reactiveEffect = () => { + if (reactiveEffect._disposed) { + return + } + if (reactiveEffect._deps) { + for (const dependencySet of reactiveEffect._deps) { + dependencySet.delete(reactiveEffect) + } + reactiveEffect._deps.clear() + } + if (reactiveEffect._cleanups) { + for (const cleanupFunction of reactiveEffect._cleanups) { + cleanupFunction() + } + reactiveEffect._cleanups.clear() + } + const previousActiveEffect = currentActiveEffect + const previousOwnerEffect = currentOwnerEffect + currentActiveEffect = reactiveEffect + currentOwnerEffect = reactiveEffect + try { + return effectFunction() + } finally { + currentActiveEffect = previousActiveEffect + currentOwnerEffect = previousOwnerEffect + } + } + objectAssign(reactiveEffect, { + _deps: null, + _cleanups: null, + _children: null, + _disposed: false, + _isComputed: isComputed, + _depth: currentActiveEffect ? currentActiveEffect._depth + 1 : 0, + _provisions: currentOwnerEffect ? currentOwnerEffect._provisions : {} + }) + if (currentOwnerEffect) { + if (!currentOwnerEffect._children) { + currentOwnerEffect._children = new Set() + } + currentOwnerEffect._children.add(reactiveEffect) + } + return reactiveEffect +} + +// --- LIFECYCLE AND CONTEXT --- +const onUnmount = (cleanupFunction) => { + if (currentOwnerEffect) { + if (!currentOwnerEffect._cleanups) { + currentOwnerEffect._cleanups = new Set() + } + currentOwnerEffect._cleanups.add(cleanupFunction) + } +} + +const onMount = (mountFunction) => { + if (currentOwnerEffect) { + if (!currentOwnerEffect._mounts) { + currentOwnerEffect._mounts = [] + } + currentOwnerEffect._mounts.push(mountFunction) + } +} + +const provide = (key, value) => { + if (currentOwnerEffect) { + currentOwnerEffect._provisions = { + ...currentOwnerEffect._provisions, + [key]: value + } + } +} + +const inject = (key, fallbackValue) => { + if (currentOwnerEffect && currentOwnerEffect._provisions[key]) { + return currentOwnerEffect._provisions[key] + } + return fallbackValue +} + +// --- SCHEDULER --- +const flushEffectQueue = () => { + if (isFlushingEffects) { + return + } + isFlushingEffects = true + const sortedEffects = Array.from(effectQueue).sort((effectA, effectB) => effectA._depth - effectB._depth) + effectQueue.clear() + for (let index = 0; index < sortedEffects.length; index++) { + const effect = sortedEffects[index] + if (!effect._disposed) { + effect() + } + } + isFlushingEffects = false +} + +const trackAndTrigger = (subscriberSet, shouldTrigger = false) => { + if (!shouldTrigger && currentActiveEffect && !currentActiveEffect._disposed) { + subscriberSet.add(currentActiveEffect) + if (!currentActiveEffect._deps) { + currentActiveEffect._deps = new Set() + } + currentActiveEffect._deps.add(subscriberSet) + } else if (shouldTrigger) { + let hasEffectsToQueue = false + for (const effect of subscriberSet) { + if (effect === currentActiveEffect || effect._disposed) { + continue + } + if (effect._isComputed) { + effect._dirty = true + if (effect._subs) { + trackAndTrigger(effect._subs, true) + } + } else { + effectQueue.add(effect) + hasEffectsToQueue = true + } + } + if (hasEffectsToQueue && !isFlushingEffects) { + queueMicrotask(flushEffectQueue) + } + } +} + +// --- SIGNALS AND COMPUTED --- +const $ = (initialValue, storageKey = null) => { + const subscribers = new Set() + // Computed case + if (isFunction(initialValue)) { + let cachedValue = undefined + let isDirty = true + const computedEffect = () => { + if (isDirty) { + const previousActiveEffect = currentActiveEffect + currentActiveEffect = computedEffect + try { + const newValue = initialValue() + if (!Object.is(cachedValue, newValue)) { + cachedValue = newValue + isDirty = false + trackAndTrigger(subscribers, true) + } + } finally { + currentActiveEffect = previousActiveEffect + } + } + trackAndTrigger(subscribers) + return cachedValue + } + objectAssign(computedEffect, { + _isComputed: true, + _subs: subscribers, + _dirty: true, + _deps: null, + _disposed: false, + stop: () => { + computedEffect._disposed = true + if (computedEffect._deps) { + for (const dependencySet of computedEffect._deps) { + dependencySet.delete(computedEffect) + } + } + subscribers.clear() + } + }) + onUnmount(computedEffect.stop) + return computedEffect + } + // Persistent $case + let currentValue = initialValue + if (storageKey) { + try { + const stored = localStorage.getItem(storageKey) + if (stored !== null) { + currentValue = JSON.parse(stored) + } + } catch (error) { + // ignore parse errors + } + } + // Signal function + const signalFunction = (...argumentsList) => { + if (argumentsList.length) { + const nextValue = isFunction(argumentsList[0]) ? argumentsList[0](currentValue) : argumentsList[0] + if (!Object.is(currentValue, nextValue)) { + currentValue = nextValue + if (storageKey) { + localStorage.setItem(storageKey, JSON.stringify(currentValue)) + } + trackAndTrigger(subscribers, true) + } + } + trackAndTrigger(subscribers) + return currentValue + } + return signalFunction +} + +// --- DOM UTILITIES WITH XSS PROTECTION --- +const setAttributeWithSecurity = (element, attributeName, attributeValue, isSvgElement) => { + let safeValue = attributeValue + if (attributeName === "src" || attributeName === "href") { + const stringValue = String(attributeValue).toLowerCase() + if (stringValue.startsWith("javascript:") || stringValue.startsWith("data:text/html")) { + safeValue = "#" + } + } + if (attributeName === "class" || attributeName === "className") { + element.className = safeValue || "" + } else if (safeValue == null || safeValue === false) { + element.removeAttribute(attributeName) + } else if (attributeName in element && !isSvgElement) { + element[attributeName] = safeValue + } else { + element.setAttribute(attributeName, safeValue === true ? "" : safeValue) + } +} + +// --- VIRTUAL DOM / TAG CREATION --- +const Tag = (tagName, properties = {}, childrenList = []) => { + // Overload: if properties is not an object, treat as children + if (properties instanceof Node || isArray(properties) || !isObject(properties)) { + childrenList = properties + properties = {} + } + const isSvgElement = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tagName) + const element = isSvgElement + ? documentReference.createElementNS("http://www.w3.org/2000/svg", tagName) + : documentReference.createElement(tagName) + + for (const [propertyName, propertyValue] of Object.entries(properties)) { + if (propertyName === "ref") { + if (isFunction(propertyValue)) { + propertyValue(element) + } else if (propertyValue && "current" in propertyValue) { + propertyValue.current = element + } + continue + } + if (propertyName.startsWith("on")) { + const eventName = propertyName.slice(2).toLowerCase() + element.addEventListener(eventName, propertyValue) + onUnmount(() => element.removeEventListener(eventName, propertyValue)) + continue + } + if (isFunction(propertyValue)) { + const effect = createReactiveEffect(() => setAttributeWithSecurity(element, propertyName, propertyValue(), isSvgElement)) + effect() + if (/^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName) && (propertyName === "value" || propertyName === "checked")) { + const inputEventName = propertyName === "checked" ? "change" : "input" + element.addEventListener(inputEventName, (event) => propertyValue(event.target[propertyName])) + } + } else { + setAttributeWithSecurity(element, propertyName, propertyValue, isSvgElement) + } + } + + const appendChildNode = (childNodeOrFunction) => { + if (isArray(childNodeOrFunction)) { + for (const nestedChild of childNodeOrFunction) { + appendChildNode(nestedChild) + } + return + } + if (isFunction(childNodeOrFunction)) { + const anchorNode = documentReference.createTextNode("") + element.appendChild(anchorNode) + let currentNodes = [] + const effect = createReactiveEffect(() => { + const result = childNodeOrFunction() + const nextNodes = (isArray(result) ? result : [result]).map(ensureNode) + for (let index = 0; index < currentNodes.length; index++) { + const existingNode = currentNodes[index] + if (existingNode._isRuntime) { + existingNode.destroy() + } else { + existingNode.remove() + } + } + let referenceNode = anchorNode + for (let index = nextNodes.length - 1; index >= 0; index--) { + const newNode = nextNodes[index] + element.insertBefore(newNode, referenceNode) + referenceNode = newNode + } + currentNodes = nextNodes + }) + effect() + return + } + element.appendChild(ensureNode(childNodeOrFunction)) + } + appendChildNode(childrenList) + return element +} + +// --- REACTIVE RENDER CONTAINER --- +const Render = (renderFunction) => { + const container = documentReference.createElement("div") + container.style.display = "contents" + const effect = createReactiveEffect(() => { + const result = renderFunction() + const nodes = (isArray(result) ? result : [result]).map(ensureNode) + for (const node of nodes) { + container.appendChild(node) + } + }) + effect() + if (effect._mounts) { + for (const mountFunction of effect._mounts) { + mountFunction() + } + } + return { + _isRuntime: true, + container: container, + destroy: () => { + disposeEffect(effect) + container.remove() + } + } +} + +// --- CONTROL FLOW COMPONENTS --- +const For = (source, itemRenderer, keyExtractor) => { + const anchorNode = documentReference.createTextNode("") + const rootContainer = Tag("div", { style: "display:contents" }, [anchorNode]) + let itemCache = new Map() + createReactiveEffect(() => { + const items = (isFunction(source) ? source() : source) || [] + const nextCache = new Map() + const order = [] + for (let index = 0; index < items.length; index++) { + const item = items[index] + const cacheKey = keyExtractor ? keyExtractor(item, index) : index + let cachedItem = itemCache.get(cacheKey) + if (!cachedItem) { + cachedItem = Render(() => itemRenderer(item, index)) + } + nextCache.set(cacheKey, cachedItem) + order.push(cacheKey) + itemCache.delete(cacheKey) + } + for (const oldItem of itemCache.values()) { + oldItem.destroy() + } + itemCache = nextCache + let referenceNode = anchorNode + for (let index = order.length - 1; index >= 0; index--) { + const key = order[index] + const view = nextCache.get(key) + if (view.container.nextSibling !== referenceNode) { + rootContainer.insertBefore(view.container, referenceNode) + } + referenceNode = view.container + } + }) + return rootContainer +} + +const If = (condition, trueBranch, falseBranch = null) => { + const anchorNode = documentReference.createTextNode("") + const rootContainer = Tag("div", { style: "display:contents" }, [anchorNode]) + let currentView = null + let lastCondition = null + createReactiveEffect(() => { + const conditionResult = !!(isFunction(condition) ? condition() : condition) + if (conditionResult === lastCondition) { + return + } + lastCondition = conditionResult + if (currentView) { + currentView.destroy() + currentView = null + } + const content = conditionResult ? trueBranch : falseBranch + if (content) { + currentView = Render(() => isFunction(content) ? content() : content) + rootContainer.insertBefore(currentView.container, anchorNode) + } + }) + return rootContainer +} + +// --- ROUTER --- +const getCurrentHashPath = () => { + return window.location.hash.slice(1) || "/" +} +const currentRoutePath = $(getCurrentHashPath()) +window.addEventListener("hashchange", () => currentRoutePath(getCurrentHashPath())) + +const Router = (routesDefinition) => { + const outletElement = Tag("div", { class: "router-outlet" }) + let currentView = null + createReactiveEffect(() => { + const path = currentRoutePath() + const pathSegments = path.split("/").filter(Boolean) + const matchedRoute = routesDefinition.find(route => { + const routeSegments = route.path.split("/").filter(Boolean) + return routeSegments.length === pathSegments.length && routeSegments.every((segment, index) => segment[0] === ":" || segment === pathSegments[index]) + }) || routesDefinition.find(route => route.path === "*") + if (matchedRoute) { + if (currentView) { + currentView.destroy() + } + const routeParams = {} + matchedRoute.path.split("/").filter(Boolean).forEach((segment, index) => { + if (segment[0] === ":") { + routeParams[segment.slice(1)] = pathSegments[index] + } + }) + Router.params(routeParams) + const componentToRender = isFunction(matchedRoute.component) ? matchedRoute.component(routeParams) : matchedRoute.component + currentView = Render(() => componentToRender) + outletElement.replaceChildren(currentView.container) + } + }) + return outletElement +} + +Router.params = $({}) +Router.to = (targetPath) => { + window.location.hash = targetPath.replace(/^#?\/?/, "#/") +} +Router.back = () => window.history.back() + +// --- MOUNT APPLICATION --- +const Mount = (component, targetSelector) => { + const targetElement = typeof targetSelector === "string" ? documentReference.querySelector(targetSelector) : targetSelector + if (!targetElement) { + return + } + const existingInstance = mountedNodesRegistry.get(targetElement) + if (existingInstance) { + existingInstance.destroy() + } + const instance = Render(isFunction(component) ? component : () => component) + targetElement.replaceChildren(instance.container) + mountedNodesRegistry.set(targetElement, instance) + return instance +} + +// --- WATCH UTILITY --- +const Watch = (sources, callback) => { + const isArraySource = isArray(sources) + const effect = createReactiveEffect(() => { + if (isArraySource) { + const previousActive = currentActiveEffect + currentActiveEffect = null + callback() + for (const dependency of sources) { + if (isFunction(dependency)) { + dependency() + } + } + currentActiveEffect = previousActive + } else { + callback() + } + }) + effect() + return () => disposeEffect(effect) +} + +// --- UNTRACK --- +const untrack = (untrackedFunction) => { + const previousActiveEffect = currentActiveEffect + currentActiveEffect = null + try { + return untrackedFunction() + } finally { + currentActiveEffect = previousActiveEffect + } +} + +// --- EXPORTS (ESM) --- +export { $, Watch, Tag, Render, If, For, Mount, onMount, onUnmount, provide, inject, Router, untrack } + +// Default export with all public members +export default { $, Watch, Tag, Render, If, For, Mount, onMount, onUnmount, provide, inject, Router, untrack } \ No newline at end of file diff --git a/sigpro v1.3.js b/sigpro v1.3.js new file mode 100644 index 0000000..f324168 --- /dev/null +++ b/sigpro v1.3.js @@ -0,0 +1,343 @@ +/** SigPro (Signals & Proxies) */ + +// Helpers +const doc = typeof document !== "undefined" ? document : null; +const isArr = Array.isArray, isFunc = f => typeof f === "function", isObj = o => o && typeof o === "object"; +const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n))); + +let activeEffect = null, activeOwner = null, isFlushing = false; +const effectQueue = new Set(), MOUNTED_NODES = new WeakMap(); + +const dispose = eff => { + if (!eff || eff._disposed) return; + eff._disposed = true; + const stack = [eff]; + while (stack.length) { + const e = stack.pop(); + if (e._cleanups) { e._cleanups.forEach(fn => fn()); e._cleanups.clear(); } + if (e._children) { e._children.forEach(child => stack.push(child)); e._children.clear(); } + if (e._deps) { e._deps.forEach(depSet => depSet.delete(e)); e._deps.clear(); } + } +}; + +export const onUnmount = fn => { + if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn); +}; + +// Effect creation +const createEffect = (fn, isComputed = false) => { + const effect = () => { + if (effect._disposed) return; + if (effect._deps) effect._deps.forEach(depSet => depSet.delete(effect)); + if (effect._cleanups) { effect._cleanups.forEach(cl => cl()); effect._cleanups.clear(); } + const prevEffect = activeEffect, prevOwner = activeOwner; + activeEffect = activeOwner = effect; + try { return isComputed ? fn() : (fn(), undefined); } + finally { activeEffect = prevEffect; activeOwner = prevOwner; } + }; + effect._deps = effect._cleanups = effect._children = null; + effect._disposed = false; + effect._isComputed = isComputed; + effect._depth = activeEffect ? activeEffect._depth + 1 : 0; + effect._mounts = []; + effect._parent = activeOwner; + effect._provisions = activeOwner ? { ...activeOwner._provisions } : {}; + if (activeOwner) (activeOwner._children ||= new Set()).add(effect); + return effect; +}; + +const flush = () => { + if (isFlushing) return; + isFlushing = true; + const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth); + effectQueue.clear(); + for (const e of sorted) if (!e._disposed) e(); + isFlushing = false; +}; + +const trackUpdate = (subs, trigger = false) => { + if (!trigger && activeEffect && !activeEffect._disposed) { + subs.add(activeEffect); + (activeEffect._deps ||= new Set()).add(subs); + } else if (trigger) { + let hasQueue = false; + subs.forEach(e => { + if (e === activeEffect || e._disposed) return; + if (e._isComputed) { + e._dirty = true; + if (e._subs) trackUpdate(e._subs, true); + } else { + effectQueue.add(e); + hasQueue = true; + } + }); + if (hasQueue && !isFlushing) queueMicrotask(flush); + } +}; + +export const untrack = fn => { const p = activeEffect; activeEffect = null; try { return fn(); } finally { activeEffect = p; } }; + +export const onMount = fn => { + if (activeOwner) (activeOwner._mounts ||= []).push(fn); +}; + +// Reactive state +export const $ = (val, key = null) => { + const subs = new Set(); + if (isFunc(val)) { + let cache, dirty = true; + const computed = () => { + if (dirty) { + const prev = activeEffect; + activeEffect = computed; + try { + const next = val(); + if (!Object.is(cache, next)) { cache = next; dirty = false; trackUpdate(subs, true); } + } finally { activeEffect = prev; } + } + trackUpdate(subs); + return cache; + }; + computed._isComputed = true; + computed._subs = subs; + computed._dirty = true; + computed._deps = null; + computed._disposed = false; + computed.markDirty = () => { dirty = true; }; + computed.stop = () => { + computed._disposed = true; + if (computed._deps) { computed._deps.forEach(depSet => depSet.delete(computed)); computed._deps.clear(); } + subs.clear(); + }; + onUnmount(computed.stop); + return computed; + } + if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val; } catch (e) { } + return (...args) => { + if (args.length) { + const next = isFunc(args[0]) ? args[0](val) : args[0]; + if (!Object.is(val, next)) { val = next; if (key) localStorage.setItem(key, JSON.stringify(val)); trackUpdate(subs, true); } + } + trackUpdate(subs); + return val; + }; +}; + +export const $$ = (obj, cache = new WeakMap()) => { + if (!isObj(obj)) return obj; + if (cache.has(obj)) return cache.get(obj); + const subs = {}; + const proxy = new Proxy(obj, { + get: (t, k) => { trackUpdate(subs[k] ??= new Set()); return isObj(t[k]) ? $$(t[k], cache) : t[k]; }, + set: (t, k, v) => { if (!Object.is(t[k], v)) { t[k] = v; if (subs[k]) trackUpdate(subs[k], true); } return true; } + }); + cache.set(obj, proxy); + return proxy; +}; + +// Watcher +export const Watch = (target, cb) => { + const explicit = isArr(target); + const effect = createEffect(() => explicit ? (untrack(cb), target.forEach(d => isFunc(d) && d())) : cb()); + effect(); + return () => dispose(effect); +}; + +const cleanupNode = node => { + if (node._cleanups) { node._cleanups.forEach(fn => fn()); node._cleanups.clear(); } + if (node._ownerEffect) dispose(node._ownerEffect); + if (node.childNodes) node.childNodes.forEach(cleanupNode); +}; + +// provide/inject +export const provide = (key, value) => { + if (activeOwner) activeOwner._provisions[key] = value; +}; + +export const inject = (key, defaultValue) => { + let ctx = activeOwner; + while (ctx) { + if (key in ctx._provisions) return ctx._provisions[key]; + ctx = ctx._parent; + } + return defaultValue; +}; + +// CreateElement +export const Tag = (tag, props = {}, children = []) => { + if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; } + const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag); + const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag); + el._cleanups = new Set(); + for (let [k, v] of Object.entries(props)) { + if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; } + if (k.startsWith("on")) { + const ev = k.slice(2).toLowerCase(); + el.addEventListener(ev, v); + const off = () => el.removeEventListener(ev, v); + el._cleanups.add(off); + onUnmount(off); + } else if (isFunc(v)) { + const effect = createEffect(() => { + const val = v(); + const safe = (k === 'src' || k === 'href') && String(val).includes('javascript:') ? '#' : val; + if (k === "class") el.className = safe || ""; + else if (safe == null || safe === false) el.removeAttribute(k); + else el.setAttribute(k, safe === true ? "" : safe); + }); + effect(); + el._cleanups.add(() => dispose(effect)); + onUnmount(() => dispose(effect)); + if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) { + const evType = k === "checked" ? "change" : "input"; + el.addEventListener(evType, ev => v(ev.target[k])); + } + } else el.setAttribute(k, v); + } + const append = c => { + if (isArr(c)) return c.forEach(append); + if (isFunc(c)) { + const anchor = doc.createTextNode(""); + el.appendChild(anchor); + let currentNodes = []; + const effect = createEffect(() => { + const res = c(); + const next = (isArr(res) ? res : [res]).map(ensureNode); + currentNodes.forEach(n => { if (n._isRuntime) n.destroy(); else cleanupNode(n); }); + let ref = anchor; + for (let i = next.length - 1; i >= 0; i--) { + const node = next[i]; + if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref); + if (node._mounts) node._mounts.forEach(fn => fn()); + ref = node; + } + currentNodes = next; + }); + effect(); + el._cleanups.add(() => dispose(effect)); + onUnmount(() => dispose(effect)); + } else { + const node = ensureNode(c); + el.appendChild(node); + if (node._mounts) node._mounts.forEach(fn => fn()); + } + }; + append(children); + return el; +}; + +// Render +export const Render = fn => { + const container = doc.createElement("div"); + container.style.display = "contents"; + const rootEffect = createEffect(() => { + const res = fn({ onCleanup: onUnmount }); + (isArr(res) ? res : [res]).forEach(r => container.appendChild(ensureNode(r))); + }); + rootEffect(); + rootEffect._mounts?.forEach(fn => fn()); + return { _isRuntime: true, container, destroy: () => { dispose(rootEffect); container.remove(); } }; +}; + +// If +export const If = (cond, t, f = null, trans = null) => { + const anchor = doc.createTextNode(""); + const root = Tag("div", { style: "display:contents" }, [anchor]); + let currentView = null, last = null; + Watch(() => { + const show = !!(isFunc(cond) ? cond() : cond); + if (show === last) return; + last = show; + const disposeView = () => { if (currentView) { currentView.destroy(); currentView = null; } }; + if (currentView && !show && trans?.out) trans.out(currentView.container, disposeView); + else disposeView(); + const content = show ? t : f; + if (content) { + currentView = Render(() => isFunc(content) ? content() : content); + root.insertBefore(currentView.container, anchor); + if (trans?.in) trans.in(currentView.container); + } + }); + return root; +}; + +// For +export const For = (src, itemFn, keyFn) => { + const anchor = doc.createTextNode(""); + const root = Tag("div", { style: "display:contents" }, [anchor]); + let cache = new Map(); + Watch(() => { + const items = (isFunc(src) ? src() : src) || []; + const next = new Map(), order = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const key = keyFn ? keyFn(item, i) : i; + let view = cache.get(key); + if (!view) view = Render(() => itemFn(item, i)); + next.set(key, view); + order.push(key); + cache.delete(key); + } + cache.forEach(v => v.destroy()); + cache = next; + let ref = anchor; + for (let i = order.length - 1; i >= 0; i--) { + const view = next.get(order[i]); + if (view.container.nextSibling !== ref) root.insertBefore(view.container, ref); + ref = view.container; + } + }); + return root; +}; + +// Router +export const Router = routes => { + const getHash = () => window.location.hash.slice(1) || "/"; + const path = $(getHash()); + const handler = () => path(getHash()); + window.addEventListener("hashchange", handler); + onUnmount(() => window.removeEventListener("hashchange", handler)); + const outlet = Tag("div", { class: "router-outlet" }); + let currentView = null; + Watch([path], () => { + const cur = path(); + const route = routes.find(r => { + const p1 = r.path.split("/").filter(Boolean); + const p2 = cur.split("/").filter(Boolean); + return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]); + }) || routes.find(r => r.path === "*"); + if (route) { + currentView?.destroy(); + const params = {}; + route.path.split("/").filter(Boolean).forEach((p, i) => { if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]; }); + Router.params(params); + currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component); + outlet.replaceChildren(currentView.container); + } + }); + return outlet; +}; +Router.params = $({}); +Router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/"); +Router.back = () => window.history.back(); +Router.path = () => window.location.hash.replace(/^#/, "") || "/"; + +// Mount +export const Mount = (comp, target) => { + const t = typeof target === "string" ? doc.querySelector(target) : target; + if (!t) return; + if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy(); + const inst = Render(isFunc(comp) ? comp : () => comp); + t.replaceChildren(inst.container); + MOUNTED_NODES.set(t, inst); + return inst; +}; + +const SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onMount, onUnmount, provide, inject }); + +export const initDX = () => { + if (typeof window === "undefined") return; + Object.assign(window, SigPro); + "div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg" + .split(" ").forEach(t => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c)); +}; \ No newline at end of file diff --git a/sigpro.deep.js b/sigpro.deep.js deleted file mode 100644 index aa8024a..0000000 --- a/sigpro.deep.js +++ /dev/null @@ -1,311 +0,0 @@ -/** - * SigPro v2.1 - Minimalista pero seguro (con árbol de owners y limpieza completa) - */ -const SigPro = (() => { - const doc = typeof document !== "undefined" ? document : null; - const isArr = Array.isArray, assign = Object.assign, isFunc = (f) => typeof f === "function", isObj = (o) => o && typeof o === "object"; - const ensureNode = (n) => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n))); - - // --- ÁRBOL DE CONTEXTO (OWNER TREE) minimalista --- - let activeOwner = null, activeEffect = null, isFlushing = false; - const effectQueue = new Set(), MOUNTED_NODES = new WeakMap(); - - const createOwner = () => { - const o = { c: new Set(), x: new Set(), d: new Set(), depth: activeOwner ? activeOwner.depth + 1 : 0 }; - if (activeOwner) activeOwner.c.add(o); - return o; - }; - - const dispose = (o) => { - if (!o) return; - o.c?.forEach(dispose); // hijos - o.x?.forEach(fn => fn()); // limpiezas - o.d?.forEach(s => s.delete(o)); // desuscribir señales - o.c?.clear(); o.x?.clear(); o.d?.clear(); - }; - - const runWithOwner = (o, cb) => { - const prev = activeOwner; activeOwner = o; - try { return cb(); } finally { activeOwner = prev; } - }; - - const onUnmount = (fn) => activeOwner?.x.add(fn); - - // --- Limpieza de nodos (simple y recursiva) --- - const cleanupNode = (node) => { - if (node._cleanups) { - node._cleanups.forEach(fn => fn()); - node._cleanups.clear(); - } - if (node._owner) dispose(node._owner); - if (node.childNodes) node.childNodes.forEach(cleanupNode); - }; - - // --- Scheduler --- - const flush = () => { - if (isFlushing) return; isFlushing = true; - const sorted = Array.from(effectQueue).sort((a, b) => a.depth - b.depth); - effectQueue.clear(); - sorted.forEach(e => !e._deleted && e.run()); - isFlushing = false; - }; - - const trackUpdate = (subs, trigger = false) => { - if (!trigger && activeEffect && !activeEffect._deleted) { - subs.add(activeEffect); - activeEffect.d.add(subs); - } else if (trigger) { - subs.forEach(e => { - if (e === activeEffect || e._deleted) return; - if (e._isComputed) { e.markDirty(); if (e._subs) trackUpdate(e._subs, true); } - else effectQueue.add(e); - }); - if (!isFlushing) queueMicrotask(flush); - } - }; - - const untrack = (fn) => { - const p = activeEffect; activeEffect = null; - try { return fn(); } finally { activeEffect = p; } - }; - - // --- CORE API (señales, efectos, reactivo) --- - const $ = (val, key = null) => { - const subs = new Set(); - if (isFunc(val)) { - let cache, dirty = true; - const e = createOwner(); - assign(e, { _isComputed: true, _subs: subs, markDirty: () => (dirty = true) }); - e.run = () => { - if (e._deleted) return; - dispose(e); - const prevE = activeEffect; activeEffect = e; - runWithOwner(e, () => { - const next = val(); - if (!Object.is(cache, next) || dirty) { cache = next; dirty = false; trackUpdate(subs, true); } - }); - activeEffect = prevE; - }; - onUnmount(() => { e._deleted = true; dispose(e); subs.clear(); }); - return () => { if (dirty) e.run(); trackUpdate(subs); return cache; }; - } - if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val; } catch(e) {} - return (...args) => { - if (args.length) { - const next = isFunc(args[0]) ? args[0](val) : args[0]; - if (!Object.is(val, next)) { - val = next; if (key) localStorage.setItem(key, JSON.stringify(val)); - trackUpdate(subs, true); - } - } - trackUpdate(subs); return val; - }; - }; - - const $$ = (obj, cache = new WeakMap()) => { - if (!isObj(obj)) return obj; - if (cache.has(obj)) return cache.get(obj); - const subs = {}; - const proxy = new Proxy(obj, { - get: (t, k) => { trackUpdate(subs[k] ??= new Set()); return isObj(t[k]) ? $$(t[k], cache) : t[k]; }, - set: (t, k, v) => { if (!Object.is(t[k], v)) { t[k] = v; if (subs[k]) trackUpdate(subs[k], true); } return true; } - }); - cache.set(obj, proxy); return proxy; - }; - - const Watch = (target, cb) => { - const explicit = isArr(target), e = createOwner(); - e.run = () => { - if (e._deleted) return; - dispose(e); - const prevE = activeEffect; activeEffect = e; - runWithOwner(e, () => { - explicit ? (untrack(cb), target.forEach(d => isFunc(d) && d())) : cb(); - }); - activeEffect = prevE; - }; - onUnmount(() => { e._deleted = true; dispose(e); }); - e.run(); - return () => { e._deleted = true; dispose(e); }; - }; - - // --- RENDERER (Tag, Render, If, For) con limpieza total --- - const Tag = (tag, props = {}, children = []) => { - if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; } - const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag); - const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag); - el._cleanups = new Set(); // para limpieza independiente - - for (let [k, v] of Object.entries(props)) { - if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; } - if (k.startsWith("on")) { - const ev = k.slice(2).toLowerCase(); - el.addEventListener(ev, v); - const off = () => el.removeEventListener(ev, v); - el._cleanups.add(off); - onUnmount(off); - } else if (isFunc(v)) { - const stop = Watch(() => { - const val = v(); - const safe = (k === 'src' || k === 'href') && String(val).includes('javascript:') ? '#' : val; - if (k === "class") el.className = safe || ""; - else if (safe == null || safe === false) el.removeAttribute(k); - else el.setAttribute(k, safe === true ? "" : safe); - }); - el._cleanups.add(stop); - onUnmount(stop); - if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) { - const evType = k === "checked" ? "change" : "input"; - el.addEventListener(evType, (ev) => v(ev.target[k])); - } - } else { - el.setAttribute(k, v); - } - } - - const append = (c) => { - if (isArr(c)) return c.forEach(append); - if (isFunc(c)) { - const anchor = doc.createTextNode(""); - el.appendChild(anchor); - let currentNodes = []; - const stop = Watch(() => { - const res = c(); - const next = (isArr(res) ? res : [res]).map(ensureNode); - // Limpiar antiguos (destruir componentes y nodos) - currentNodes.forEach(n => { - if (n._isRuntime) n.destroy(); - else cleanupNode(n); - }); - let ref = anchor; - for (let i = next.length-1; i >= 0; i--) { - const node = next[i]; - if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref); - ref = node; - } - currentNodes = next; - }); - el._cleanups.add(stop); - onUnmount(stop); - } else { - el.appendChild(ensureNode(c)); - } - }; - append(children); - return el; - }; - - const Render = (fn) => { - const root = createOwner(); - const container = doc.createElement("div"); - container.style.display = "contents"; - runWithOwner(root, () => { - const res = fn({ onCleanup: onUnmount }); - (isArr(res) ? res : [res]).forEach(r => container.appendChild(ensureNode(r))); - }); - return { _isRuntime: true, container, destroy: () => { dispose(root); container.remove(); } }; - }; - - const If = (cond, t, f = null, trans = null) => { - const anchor = doc.createTextNode(""); - const root = Tag("div", { style: "display:contents" }, [anchor]); - let currentView = null, last = null; - Watch(() => { - const show = !!(isFunc(cond) ? cond() : cond); - if (show === last) return; last = show; - const disposeView = () => { if (currentView) { currentView.destroy(); currentView = null; } }; - if (currentView && !show && trans?.out) trans.out(currentView.container, disposeView); - else disposeView(); - const content = show ? t : f; - if (content) { - currentView = Render(() => isFunc(content) ? content() : content); - root.insertBefore(currentView.container, anchor); - if (trans?.in) trans.in(currentView.container); - } - }); - return root; - }; - - const For = (src, itemFn, keyFn) => { - const anchor = doc.createTextNode(""); - const root = Tag("div", { style: "display:contents" }, [anchor]); - let cache = new Map(); - Watch(() => { - const items = (isFunc(src) ? src() : src) || []; - const next = new Map(); - const order = []; - for (let i = 0; i < items.length; i++) { - const item = items[i]; - const key = keyFn ? keyFn(item, i) : i; - let view = cache.get(key); - if (!view) view = Render(() => itemFn(item, i)); - next.set(key, view); - order.push(key); - cache.delete(key); - } - cache.forEach(v => v.destroy()); - cache = next; - let ref = anchor; - for (let i = order.length-1; i >= 0; i--) { - const view = next.get(order[i]); - if (view.container.nextSibling !== ref) root.insertBefore(view.container, ref); - ref = view.container; - } - }); - return root; - }; - - // --- Router (con limpieza del evento global) --- - const Router = (routes) => { - const getHash = () => window.location.hash.slice(1) || "/"; - const path = $(getHash()); - const handler = () => path(getHash()); - window.addEventListener("hashchange", handler); - onUnmount(() => window.removeEventListener("hashchange", handler)); - - const outlet = Tag("div", { class: "router-outlet" }); - let currentView = null; - Watch([path], () => { - const cur = path(); - const route = routes.find(r => { - const p1 = r.path.split("/").filter(Boolean); - const p2 = cur.split("/").filter(Boolean); - return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]); - }) || routes.find(r => r.path === "*"); - if (route) { - currentView?.destroy(); - const params = {}; - route.path.split("/").filter(Boolean).forEach((p, i) => { - if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]; - }); - Router.params(params); - currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component); - outlet.replaceChildren(currentView.container); - } - }); - return outlet; - }; - Router.params = $({}); - Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/"); - Router.back = () => window.history.back(); - Router.path = () => window.location.hash.replace(/^#/, "") || "/"; - - const Mount = (comp, target) => { - const t = typeof target === "string" ? doc.querySelector(target) : target; - if (!t) return; - if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy(); - const inst = Render(isFunc(comp) ? comp : () => comp); - t.replaceChildren(inst.container); - MOUNTED_NODES.set(t, inst); - return inst; - }; - - return { $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onUnmount }; -})(); - -if (typeof window !== "undefined") { - Object.assign(window, SigPro); - "div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg" - .split(" ").forEach(t => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c)); -} -export default SigPro; \ No newline at end of file diff --git a/sigpro_gemini.js b/sigpro_gemini.js deleted file mode 100644 index a560972..0000000 --- a/sigpro_gemini.js +++ /dev/null @@ -1,261 +0,0 @@ -/** - * SigPro v2 G - */ -const SigPro = (() => { - const doc = typeof document !== "undefined" ? document : null; - const isArr = Array.isArray, assign = Object.assign, isFunc = (f) => typeof f === "function", isObj = (o) => typeof o === "object" && o !== null; - const ensureNode = (n) => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(String(n ?? ""))); - - // --- ÁRBOL DE CONTEXTO (OWNER TREE) --- - // c: Hijos (children), x: Limpiezas (cleanups), d: Dependencias/Señales (deps) - let activeOwner = null, activeEffect = null, isFlushing = false; - const effectQueue = new Set(), MOUNTED_NODES = new WeakMap(); - - const createOwner = () => { - const o = { c: new Set(), x: new Set(), d: new Set(), depth: activeOwner ? activeOwner.depth + 1 : 0 }; - if (activeOwner) activeOwner.c.add(o); - return o; - }; - - const dispose = (o) => { - if (!o) return; - o.c?.forEach(dispose); // 1. Destruye hijos recursivamente - o.x?.forEach(f => f()); // 2. Ejecuta limpiezas (eventos, timers) - o.d?.forEach(s => s.delete(o)); // 3. Se desuscribe de las señales - o.c?.clear(); o.x?.clear(); o.d?.clear(); - }; - - const runWithOwner = (o, cb) => { - const prev = activeOwner; activeOwner = o; - try { return cb(); } finally { activeOwner = prev; } - }; - - const onUnmount = (fn) => activeOwner?.x.add(fn); - - // --- SCHEDULER --- - const flush = () => { - if (isFlushing) return; isFlushing = true; - const sorted = Array.from(effectQueue).sort((a, b) => a.depth - b.depth); - effectQueue.clear(); - sorted.forEach(e => !e._deleted && e.run()); - isFlushing = false; - }; - - const trackUpdate = (subs, trigger = false) => { - if (!trigger && activeEffect && !activeEffect._deleted) { - subs.add(activeEffect); activeEffect.d.add(subs); - } else if (trigger) { - subs.forEach(e => { - if (e === activeEffect || e._deleted) return; - if (e._isComputed) { e.markDirty(); if (e._subs) trackUpdate(e._subs, true); } - else effectQueue.add(e); - }); - if (!isFlushing) queueMicrotask(flush); - } - }; - - const untrack = (fn) => { - const p = activeEffect; activeEffect = null; - try { return fn(); } finally { activeEffect = p; } - }; - - // --- CORE API --- - const $ = (val, key = null) => { - const subs = new Set(); - if (isFunc(val)) { - let cache, dirty = true; - const e = createOwner(); - assign(e, { _isComputed: true, _subs: subs, markDirty: () => (dirty = true) }); - - e.run = () => { - if (e._deleted) return; - dispose(e); // Limpia la ejecución anterior - const prevE = activeEffect; activeEffect = e; - runWithOwner(e, () => { - const next = val(); - if (!Object.is(cache, next) || dirty) { cache = next; dirty = false; trackUpdate(subs, true); } - }); - activeEffect = prevE; - }; - - onUnmount(() => { e._deleted = true; dispose(e); subs.clear(); }); - return () => { if (dirty) e.run(); trackUpdate(subs); return cache; }; - } - if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val; } catch (e) { } - return (...args) => { - if (args.length) { - const next = isFunc(args[0]) ? args[0](val) : args[0]; - if (!Object.is(val, next)) { - val = next; if (key) localStorage.setItem(key, JSON.stringify(val)); - trackUpdate(subs, true); - } - } - trackUpdate(subs); return val; - }; - }; - - const $$ = (obj, cache = new WeakMap()) => { - if (!isObj(obj)) return obj; - if (cache.has(obj)) return cache.get(obj); - const subs = {}; - const proxy = new Proxy(obj, { - get: (t, k) => { trackUpdate(subs[k] ??= new Set()); return isObj(t[k]) ? $$(t[k], cache) : t[k]; }, - set: (t, k, v) => { if (!Object.is(t[k], v)) { t[k] = v; if (subs[k]) trackUpdate(subs[k], true); } return true; } - }); - cache.set(obj, proxy); return proxy; - }; - - const Watch = (target, cb) => { - const explicit = isArr(target), e = createOwner(); - e.run = () => { - if (e._deleted) return; - dispose(e); - const prevE = activeEffect; activeEffect = e; - runWithOwner(e, () => { - explicit ? (untrack(cb), target.forEach(d => isFunc(d) && d())) : cb(); - }); - activeEffect = prevE; - }; - onUnmount(() => { e._deleted = true; dispose(e); }); - e.run(); return () => { e._deleted = true; dispose(e); }; - }; - - const Tag = (tag, props = {}, children = []) => { - if (props instanceof Node || isArr(props) || !isObj(props)) { children = props; props = {}; } - const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag); - const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag); - - for (let [k, v] of Object.entries(props)) { - if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; } - if (k.startsWith("on")) { - const ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v); - onUnmount(() => el.removeEventListener(ev, v)); // Se adjunta al Owner activo! - } else if (isFunc(v)) { - Watch(() => { - const val = v(), safe = (k === 'src' || k === 'href') && String(val).includes('javascript:') ? '#' : val; - k === "class" ? (el.className = safe || "") : (safe == null || safe === false ? el.removeAttribute(k) : el.setAttribute(k, safe === true ? "" : safe)); - }); - if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) { - el.addEventListener(k === "checked" ? "change" : "input", (ev) => v(ev.target[k])); - } - } else el.setAttribute(k, v); - } - - const append = (c) => { - if (isArr(c)) return c.forEach(append); - if (isFunc(c)) { - const m = doc.createTextNode(""); el.appendChild(m); let curr = []; - Watch(() => { - const res = c(), next = (isArr(res) ? res : [res]).map(ensureNode); - curr.forEach(n => { if (n instanceof Node) n.remove(); }); // Adiós cleanupNode() - next.forEach(n => m.parentNode?.insertBefore(n, m)); curr = next; - }); - } else el.appendChild(ensureNode(c)); - }; - append(children); return el; - }; - - const Render = (fn) => { - const root = createOwner(), container = doc.createElement("div"); - container.style.display = "contents"; - runWithOwner(root, () => { - const res = fn({ onCleanup: onUnmount }); - (isArr(res) ? res : [res]).forEach(r => container.appendChild(ensureNode(r))); - }); - return { _isRuntime: true, container, destroy: () => { dispose(root); container.remove(); } }; - }; - - const If = (cond, t, f = null, trans = null) => { - const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]); - let view = null, last = null; - Watch(() => { - const s = !!(isFunc(cond) ? cond() : cond); - if (s === last) return; last = s; - const disposeView = () => { if (view) { view.destroy(); view = null; } }; - if (view && !s && trans?.out) trans.out(view.container, disposeView); else disposeView(); - const b = s ? t : f; - if (b) { - view = Render(() => isFunc(b) ? b() : b); - root.insertBefore(view.container, m); - if (trans?.in) trans.in(view.container); - } - }); - return root; - }; - - const For = (src, itemFn, keyFn) => { - const m = doc.createTextNode(""), root = Tag("div", { style: "display:contents" }, [m]); - let cache = new Map(); - Watch(() => { - const items = (isFunc(src) ? src() : src) || [], next = new Map(), order = []; - items.forEach((item, i) => { - const k = keyFn ? keyFn(item, i) : i; - let v = cache.get(k) || Render(() => itemFn(item, i)); - cache.delete(k); next.set(k, v); order.push(k); - }); - cache.forEach(v => v.destroy()); - let anchor = m; - for (let i = order.length - 1; i >= 0; i--) { - const v = next.get(order[i]); - if (v.container.nextSibling !== anchor) root.insertBefore(v.container, anchor); - anchor = v.container; - } - cache = next; - }); - return root; - }; - - const Router = (routes) => { - const getHash = () => window.location.hash.slice(1) || "/"; - const path = $(getHash()); - const listener = () => path(getHash()); - window.addEventListener("hashchange", listener); - onUnmount(() => window.removeEventListener("hashchange", listener)); // Ahora el router se limpia solo - - const outlet = Tag("div", { class: "router-outlet" }); - let currentView = null; - - Watch([path], () => { - const cur = path(); - const route = routes.find(r => { - const p1 = r.path.split("/").filter(Boolean); - const p2 = cur.split("/").filter(Boolean); - return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]); - }) || routes.find(r => r.path === "*"); - - if (route) { - currentView?.destroy(); - const params = {}; - route.path.split("/").filter(Boolean).forEach((p, i) => { - if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]; - }); - - Router.params(params); - currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component); - outlet.replaceChildren(currentView.container); - } - }); - return outlet; - }; - - Router.params = $({}); - Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/"); - Router.back = () => window.history.back(); - Router.path = () => window.location.hash.replace(/^#/, "") || "/"; - - const Mount = (comp, target) => { - const t = typeof target === "string" ? doc.querySelector(target) : target; - if (!t) return; if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy(); - const inst = Render(isFunc(comp) ? comp : () => comp); - t.replaceChildren(inst.container); MOUNTED_NODES.set(t, inst); return inst; - }; - - return { $, $$, Watch, Tag, Render, If, For, Router, Mount, untrack, onUnmount }; -})(); - -if (typeof window !== "undefined") { - Object.assign(window, SigPro); - "div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg" - .split(" ").forEach(t => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c)); -} -export default SigPro; \ No newline at end of file diff --git a/sigworkPro.js b/sigworkPro.js new file mode 100644 index 0000000..4875d17 --- /dev/null +++ b/sigworkPro.js @@ -0,0 +1,438 @@ +const isFunction = (v) => typeof v === 'function'; +const isNode = (v) => v instanceof Node; +const doc = typeof document !== "undefined" ? document : null; + +let activeEffect = null; +const pendingEffects = new Set(); +let flushScheduled = false; + +const flushEffects = () => { + if (pendingEffects.size === 0) return; + const all = Array.from(pendingEffects); + pendingEffects.clear(); + all.sort((a, b) => a.depth - b.depth); + for (let i = 0; i < all.length; i++) { + const e = all[i]; + if (!e.disposed) e.execute(); + } + flushScheduled = false; +}; + +const scheduleFlush = () => { + if (!flushScheduled) { + flushScheduled = true; + queueMicrotask(flushEffects); + } +}; + +const disposeEffectTree = (effect) => { + if (effect.disposed) return; + effect.disposed = true; + const stack = [effect]; + while (stack.length) { + const cur = stack.pop(); + if (cur.cleanups) { + for (const fn of cur.cleanups) fn(); + cur.cleanups.clear(); + } + if (cur.dependencies) { + for (const depSet of cur.dependencies) depSet.delete(cur); + cur.dependencies.clear(); + } + if (cur.children) { + for (const child of cur.children) stack.push(child); + cur.children.clear(); + } + } +}; + +const createEffect = (fn) => { + const effect = { + execute: null, + dependencies: new Set(), + cleanups: new Set(), + children: new Set(), + depth: activeEffect ? activeEffect.depth + 1 : 0, + disposed: false, + }; + effect.execute = () => { + if (effect.disposed) return; + if (effect.dependencies) { + for (const depSet of effect.dependencies) depSet.delete(effect); + effect.dependencies.clear(); + } + if (effect.cleanups) { + for (const fn of effect.cleanups) fn(); + effect.cleanups.clear(); + } + const prev = activeEffect; + activeEffect = effect; + try { + const cleanup = fn(); + if (isFunction(cleanup)) effect.cleanups.add(cleanup); + } finally { + activeEffect = prev; + } + }; + if (activeEffect) activeEffect.children.add(effect); + effect.execute(); + return () => disposeEffectTree(effect); +}; + +export const Watch = createEffect; +export const effect = Watch; +export const scope = Watch; + +const track = (subs) => { + if (activeEffect && !activeEffect.disposed) { + subs.add(activeEffect); + activeEffect.dependencies.add(subs); + } +}; + +const trigger = (subs) => { + if (!subs) return; + for (const eff of subs) { + if (eff !== activeEffect && !eff.disposed) pendingEffects.add(eff); + } + scheduleFlush(); +}; + +export const $ = (initialValue) => { + const subs = new Set(); + return { + get value() { + track(subs); + return initialValue; + }, + set value(newVal) { + if (Object.is(newVal, initialValue)) return; + initialValue = newVal; + trigger(subs); + }, + }; +}; +export const signal = $; + +export const persistent = (initialValue, storageKey) => { + let stored = initialValue; + try { + const item = localStorage.getItem(storageKey); + if (item !== null) stored = JSON.parse(item); + } catch (e) {} + const sig = $(stored); + Watch(() => { + const val = sig.value; + try { localStorage.setItem(storageKey, JSON.stringify(val)); } catch (e) {} + }); + return sig; +}; + +export const computed = (fn) => { + const s = $(); + Watch(() => { s.value = fn(); }); + return { get value() { return s.value; } }; +}; + +export const untrack = (fn) => { + const prev = activeEffect; + activeEffect = null; + try { return fn(); } finally { activeEffect = prev; } +}; + +export const watch = (source, callback) => { + let first = true, oldVal; + return Watch(() => { + const newVal = isFunction(source) ? source() : source.value; + if (!first) untrack(() => callback(newVal, oldVal)); + else first = false; + oldVal = newVal; + }); +}; + +let currentComponentContext = null; + +export const onMount = (fn) => { + if (currentComponentContext) currentComponentContext.mount.push(fn); +}; +export const onUnmount = (fn) => { + if (currentComponentContext) currentComponentContext.unmount.push(fn); +}; +export const provide = (key, val) => { + if (currentComponentContext) currentComponentContext.provisions[key] = val; +}; +export const inject = (key, def) => { + if (currentComponentContext && key in currentComponentContext.provisions) + return currentComponentContext.provisions[key]; + return def; +}; + +const setProperty = (el, key, val, isSVG) => { + if ((key === 'src' || key === 'href') && typeof val === 'string') { + const lower = val.toLowerCase(); + if (lower.startsWith('javascript:') || lower.startsWith('data:text/html')) { + console.warn(`Bloqueado ${key}`); + val = '#'; + } + } + if (key === 'class' || key === 'className') { + el.className = val || ''; + } else if (key === 'style' && typeof val === 'object') { + Object.assign(el.style, val); + } else if (key in el && !isSVG) { + el[key] = val; + } else { + if (val == null || val === false) el.removeAttribute(key); + else if (val === true) el.setAttribute(key, ''); + else el.setAttribute(key, val); + } +}; + +const appendChildNode = (parent, child) => { + if (child == null) return; + if (isFunction(child)) { + const anchor = doc.createTextNode(''); + parent.appendChild(anchor); + let currentNodes = []; + Watch(() => { + const raw = child(); + const next = (Array.isArray(raw) ? raw : [raw]) + .flat(Infinity) + .filter(v => v != null) + .map(v => isNode(v) ? v : doc.createTextNode(String(v))); + for (let i = 0; i < currentNodes.length; i++) { + const n = currentNodes[i]; + if (!next.includes(n)) removeNode(n); + } + let ref = anchor; + for (let i = next.length - 1; i >= 0; i--) { + const n = next[i]; + if (n.parentNode !== parent) { + parent.insertBefore(n, ref); + if (n.componentContext) n.componentContext.mount.forEach(fn => fn()); + } + ref = n; + } + currentNodes = next; + }); + } else if (isNode(child)) { + parent.appendChild(child); + } else { + parent.appendChild(doc.createTextNode(String(child))); + } +}; + +const removeNode = (node) => { + if (node.componentStop) node.componentStop(); + if (node.componentContext) node.componentContext.unmount.forEach(fn => fn()); + if (node.leaveTransition) { + node.leaveTransition(() => node.remove()); + } else { + node.remove(); + } +}; + +export const Tag = (tag, props, ...children) => { + props = props || {}; + children = children.flat(Infinity); + if (isFunction(tag)) { + const prevCtx = currentComponentContext; + const ctx = { + mount: [], + unmount: [], + provisions: { ...(prevCtx?.provisions || {}) }, + }; + currentComponentContext = ctx; + let rendered; + const stop = scope(() => { + rendered = tag(props, { + children, + emit: (ev, ...args) => { + const handler = props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]; + if (isFunction(handler)) handler(...args); + }, + }); + }); + currentComponentContext = prevCtx; + if (isNode(rendered) || isFunction(rendered)) { + rendered.componentContext = ctx; + rendered.componentStop = stop; + } + return rendered; + } + if (!tag) return () => children; + const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|use)$/.test(tag); + const el = isSVG + ? doc.createElementNS('http://www.w3.org/2000/svg', tag) + : doc.createElement(tag); + for (const [k, v] of Object.entries(props)) { + if (k === 'ref') { + if (isFunction(v)) v(el); + else if (v && 'value' in v) v.value = el; + continue; + } + if (k.startsWith('on')) { + const ev = k.slice(2).toLowerCase(); + el.addEventListener(ev, v); + onUnmount(() => el.removeEventListener(ev, v)); + continue; + } + if (isFunction(v)) { + Watch(() => setProperty(el, k, v(), isSVG)); + } else { + setProperty(el, k, v, isSVG); + } + } + for (const child of children) appendChildNode(el, child); + return el; +}; +export const h = Tag; + +export const If = ({ when, children }) => { + return () => (isFunction(when) ? when() : when) ? children[0] : children[1] || null; +}; + +export const For = ({ each, key, children }) => { + let cache = new Map(); + return () => { + const items = isFunction(each) ? each() : each || []; + const newCache = new Map(); + const nodes = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const itemKey = key ? (isFunction(key) ? key(item, i) : item[key]) : i; + let node = cache.get(itemKey); + if (!node) { + const childFn = children[0]; + node = Tag(childFn, { item, index: i }); + } + newCache.set(itemKey, node); + nodes.push(node); + } + cache = newCache; + return nodes; + }; +}; + +export const Transition = ({ enter, leave, children }) => { + const decorate = (el) => { + if (!isNode(el)) return el; + if (enter) { + const [from, active, to] = enter; + requestAnimationFrame(() => { + el.classList.add(active); + requestAnimationFrame(() => { + el.classList.add(from); + el.classList.remove(active); + el.classList.add(to); + const onEnd = () => { + el.classList.remove(to, from); + el.removeEventListener('transitionend', onEnd); + }; + el.addEventListener('transitionend', onEnd, { once: true }); + }); + }); + } + if (leave) { + const [from, active, to] = leave; + el.leaveTransition = (done) => { + el.classList.add(active); + requestAnimationFrame(() => { + el.classList.add(from); + el.classList.remove(active); + el.classList.add(to); + const onEnd = () => { + el.classList.remove(to, from); + el.removeEventListener('transitionend', onEnd); + done(); + }; + el.addEventListener('transitionend', onEnd, { once: true }); + }); + }; + } + return el; + }; + const child = children[0]; + if (!child) return null; + return isFunction(child) ? () => decorate(child()) : decorate(child); +}; + +const currentPath = $((window.location.hash.slice(1) || '/')); +window.addEventListener('hashchange', () => { + currentPath.value = window.location.hash.slice(1) || '/'; +}); + +export const Router = ({ routes }) => { + const outlet = Tag('div', { class: 'router-outlet' }); + let currentView = null; + Watch(() => { + const path = currentPath.value; + const segments = path.split('/').filter(Boolean); + const matched = routes.find(route => { + const rSeg = route.path.split('/').filter(Boolean); + return rSeg.length === segments.length && rSeg.every((s, i) => s[0] === ':' || s === segments[i]); + }) || routes.find(r => r.path === '*'); + if (matched) { + if (currentView && currentView.componentStop) currentView.componentStop(); + const params = {}; + const rSeg = matched.path.split('/').filter(Boolean); + rSeg.forEach((s, i) => { if (s[0] === ':') params[s.slice(1)] = segments[i]; }); + currentView = Tag(matched.component, { params }); + outlet.innerHTML = ''; + outlet.appendChild(currentView); + } + }); + return outlet; +}; + +export const navigate = (to) => { window.location.hash = to.replace(/^#?\/?/, '#/'); }; +export const back = () => window.history.back(); +export const getCurrentPath = () => currentPath.value; + +export const $$ = (obj, cache = new WeakMap()) => { + if (!obj || typeof obj !== 'object') return obj; + if (cache.has(obj)) return cache.get(obj); + const subs = {}; + const proxy = new Proxy(obj, { + get: (t, k) => { + track(subs[k] ??= new Set()); + const val = t[k]; + return (val && typeof val === 'object') ? $$(val, cache) : val; + }, + set: (t, k, v) => { + if (Object.is(t[k], v)) return true; + t[k] = v; + if (subs[k]) trigger(subs[k]); + return true; + }, + }); + cache.set(obj, proxy); + return proxy; +}; +export const reactive = $$; + +export const createApp = (Root, rootProps = {}) => (selector) => { + const target = typeof selector === 'string' ? doc.querySelector(selector) : selector; + if (!target) throw new Error(`No se encontró ${selector}`); + if (target.appUnmount) target.appUnmount(); + const app = Tag(Root, rootProps); + target.appendChild(app); + if (app.componentContext) app.componentContext.mount.forEach(fn => fn()); + target.appUnmount = () => removeNode(app); + return target.appUnmount; +}; + +const tags = 'div span p a button input form label ul li ol header footer main section article nav aside h1 h2 h3 h4 h5 h6 img svg path circle rect line polyline polygon g defs text use br hr pre code strong em table tr td th thead tbody tfoot select option textarea iframe video audio canvas'.split(' '); +tags.forEach(tag => { + const name = tag[0].toUpperCase() + tag.slice(1); + globalThis[name] = (props, ...children) => Tag(tag, props, ...children); +}); + +export const createElement = Tag; +export const Fragment = (props) => props.children; + +export default { + $, signal, $$, reactive, persistent, computed, Watch, effect, scope, watch, untrack, + onMount, onUnmount, provide, inject, Tag, h, createElement, Fragment, If, For, Transition, + Router, navigate, back, getCurrentPath, createApp, +}; \ No newline at end of file