import { isRef, ref, watch } from './reactivity.js'; import type { AnyFunction, RuntimeContext, PropOptions } from './types'; export function guessValue(s: string) { s = s.trim(); if (s === 'true') { return true; } if (s === 'false') { return false; } try { return Function('return ' + s)(); } catch { return s; } } const validAttribute = /^[a-zA-Z_][a-zA-Z0-9\-_:.]*$/; export const isValidAttribute = (s) => validAttribute.test(s); function getPropValue(element: Element, name: T, defaultValue: any) { const value = element[name]; if (value !== undefined) { return value; } const attr = element.getAttribute(name); if (attr !== null) { return guessValue(attr); } if (defaultValue !== undefined) { return typeof defaultValue === 'function' ? defaultValue() : defaultValue; } } export function walkDomTree(tree: Node, fn: AnyFunction, context: any) { const stack: Node[] = tree.childNodes ? Array.from(tree.childNodes) : []; let node; while ((node = stack.shift() as Node)) { fn(node, context); if (node.nodeType === node.ELEMENT_NODE && !(node as any).hasAttribute('do-not-render') && node.childNodes.length) { stack.push(...(Array.from(node.childNodes) as any[])); } } } export function createFunction(expression: string, context: any, args: string[] = []) { const k = Object.keys(context) .filter((key: any) => expression.includes(key)) .join(', ') .trim(); return Function(...args, (k ? `const { ${k} } = this;` : '') + `return ${expression};`).bind(context); } export function createReadOnlyContext(context: any) { return new Proxy(context, { get(target, key) { const t = target[key]; if (t !== undefined) { if (t && isRef(t)) { return t.value; } return t; } }, set() { throw new Error('View contexts are read-only'); }, }); } const runtimeStack: RuntimeContext[] = []; export function getCurrentNode() { const t = runtimeStack.at(-1); if (!t) { throw new Error('Missing context for this component'); } return t; } export function createContext(element: Element, setup: any, dom: DocumentFragment) { const runtime: RuntimeContext = { dom, context: null, element, mount: [], update: [], unmount: [], props: {}, refs: {}, }; runtimeStack.push(runtime); try { runtime.context = setup(); } catch (e) { console.error(e); } finally { runtime.context ||= {}; runtimeStack.pop(); } return runtime; } export const debounce = (fn: any) => { let timer: any = 0; return function (...args: any[]) { clearTimeout(timer); timer = setTimeout(() => fn(...args), 1); }; }; const stylesheetCache = new Map>(); export function importCssModule(href: string): Promise { if (!stylesheetCache.has(href)) { stylesheetCache.set(href, importCssModuleInternal(href)); } return stylesheetCache.get(href)!; } let _importCssModule: any = importModuleFromSource( 'export default function(href) { return import(href, { with: { type: "css" } }) }', ); async function importCssModuleInternal(href: string) { if (typeof _importCssModule !== 'function') { _importCssModule = (await _importCssModule).default; } try { return (await _importCssModule(href)).default as CSSStyleSheet; } catch { const sheet = new CSSStyleSheet(); sheet.replaceSync(`@import url(${href})`); return sheet; } } export async function importModuleFromSource(sourceText: string, origin?: string) { let fileName; if (origin) { fileName = String(origin).replace('.html', '.mjs'); const originalFile = new URL(fileName, 'https://li3.dev'); originalFile.pathname = originalFile.pathname.replace('.mjs', '.src.mjs'); const lineCount = sourceText.split(/\r?\n/).length; const mappings = new Array(lineCount).fill('AACA'); mappings[0] = 'AAAA'; const sourceMap = { version: 3, file: fileName, sourcesContent: [sourceText], sources: [String(originalFile)], mappings: mappings.join(';'), }; const jsonString = JSON.stringify(sourceMap); const base64Map = btoa(unescape(encodeURIComponent(jsonString))); const mapUrl = `data:application/json;charset=utf-8;base64,${base64Map}`; sourceText += `\n//# sourceMappingURL=${mapUrl}\n//# sourceURL=${fileName}`; } const blob = new Blob([sourceText], { type: 'application/javascript' }); const objectUrl = URL.createObjectURL(blob); try { return await import(objectUrl); } catch (error) { if (origin && error instanceof Error && error.stack) { const blobUrlPattern = new RegExp(objectUrl, 'g'); error.stack = error.stack.replace(blobUrlPattern, fileName); } throw error; } finally { URL.revokeObjectURL(objectUrl); } } export const toCamelCase = (s) => s.replace(/-([a-z])/g, (_: any, letter: string) => letter.toUpperCase()); export function eventEmitter(element, name, value) { const event = new CustomEvent(name, { detail: value }); const handler = element['on' + name]; if (typeof handler === 'function') { handler(event); } element.dispatchEvent(event); return event; } export function definePropInternal(name: string, options: PropOptions = {}) { const { element, update, props } = getCurrentNode(); const current = getPropValue(element, name as any, options.default); const prop = ref(current); const attribute = options.attribute && isValidAttribute(name); watch(prop, (value: any) => { if (element[name] !== value) { element[name] = value; } }); Object.defineProperty(element, name, { get() { return prop.value; }, set(value) { prop.value = value; for (const fn of update) { fn(); } if (attribute) { element.setAttribute(name, String(value)); } }, }); props[name] = prop; return prop; }