UNPKG

@magnusbag/livets-core

Version:

TypeScript API layer for LiveTS framework

252 lines 7.83 kB
"use strict"; /** * Utility functions for LiveTS */ Object.defineProperty(exports, "__esModule", { value: true }); exports.escapeHtml = escapeHtml; exports.classNames = classNames; exports.html = html; exports.raw = raw; exports.debounce = debounce; exports.throttle = throttle; exports.deepMerge = deepMerge; exports.isValidSelector = isValidSelector; exports.extractDataAttributes = extractDataAttributes; exports.generateId = generateId; exports.isBrowser = isBrowser; exports.isServer = isServer; exports.injectReactiveSelectors = injectReactiveSelectors; exports.injectReactiveSelectorsSmart = injectReactiveSelectorsSmart; exports.calculateSavings = calculateSavings; /** * Escapes HTML to prevent XSS attacks */ function escapeHtml(unsafe) { return unsafe .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#039;'); } /** * Generates a CSS class string from an object */ function classNames(classes) { return Object.entries(classes) .filter(([_, condition]) => condition) .map(([className]) => className) .join(' '); } /** * Creates a reactive template function for rendering */ function html(strings, ...values) { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; if (i < values.length) { const value = values[i]; // Escape HTML by default unless it's marked as safe if (typeof value === 'string') { result += escapeHtml(value); } else if (value !== null && value !== undefined && typeof value.toString === 'function') { result += escapeHtml(value.toString()); } else if (value === 0) { result += '0'; } else { result += ''; } } } return result; } /** * Marks HTML as safe (bypasses escaping) */ function raw(html) { return new SafeHtml(html); } class SafeHtml { constructor(html) { this.html = html; } toString() { return this.html; } } /** * Debounces a function call */ function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), wait); }; } /** * Throttles a function call */ function throttle(func, limit) { let inThrottle; return (...args) => { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; } /** * Deep merges two objects */ function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { const sourceValue = source[key]; const targetValue = result[key]; if (sourceValue && typeof sourceValue === 'object' && !Array.isArray(sourceValue) && targetValue && typeof targetValue === 'object' && !Array.isArray(targetValue)) { result[key] = deepMerge(targetValue, sourceValue); } else { result[key] = sourceValue; } } } return result; } /** * Validates that a string is a valid CSS selector */ function isValidSelector(selector) { if (typeof document === 'undefined') { // Server-side: basic validation return selector.length > 0 && !selector.includes('<'); } try { document.querySelector(selector); return true; } catch { return false; } } /** * Extracts data attributes from an element */ function extractDataAttributes(attributes) { const data = {}; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith('data-')) { const dataKey = key.slice(5); // Remove 'data-' prefix // Try to parse as JSON, fallback to string try { data[dataKey] = JSON.parse(value); } catch { data[dataKey] = value; } } } return data; } /** * Generates a unique ID */ function generateId(prefix = 'livets') { return `${prefix}-${Math.random().toString(36).substr(2, 9)}`; } /** * Checks if code is running in the browser */ function isBrowser() { return (typeof globalThis !== 'undefined' && typeof globalThis.window !== 'undefined' && typeof globalThis.document !== 'undefined'); } /** * Checks if code is running on the server */ function isServer() { return !isBrowser(); } /** * Injects data-ts-selector attributes into reactive HTML elements * Identifies elements with ts-on: attributes and common reactive patterns */ function injectReactiveSelectors(html, componentId) { let selectorCounter = 0; // Add selectors to elements with ts-on: attributes (these are clearly reactive) let processedHtml = html.replace(/<([a-zA-Z][a-zA-Z0-9-]*)\s+([^>]*?ts-on:[^>]*?)>/g, (match, tagName, attributes) => { // Check if already has data-ts-selector if (attributes.includes('data-ts-sel=')) { return match; } const selector = `${componentId}-sel-${selectorCounter++}`; return `<${tagName} data-ts-sel="${selector}" ${attributes}>`; }); return processedHtml; } /** * Enhanced version that also marks elements likely to contain reactive content * This should be called with component instance context for better detection */ function injectReactiveSelectorsSmart(html, componentId, stateKeys = []) { let selectorCounter = 0; // Create a short component hash (8 chars instead of 36 char UUID) const componentHash = createShortHash(componentId); // Process all elements that need selectors in one pass // This avoids the issue of elements with both ts-on and class attributes let processedHtml = html.replace(/<([a-zA-Z][a-zA-Z0-9-]*)([\s\S]*?)>/g, (match, tagName, attributes) => { // Skip if already has data-ts-selector if (attributes.includes('data-ts-sel=')) { return match; } // Add selector if element has ts-on: attributes OR class attributes const hasInteraction = attributes.includes('ts-on:'); const hasClass = attributes.includes('class='); if (hasInteraction || hasClass) { // Ultra-short selector: 8-char hash + base36 counter const selector = `${componentHash}.${selectorCounter.toString(36)}`; selectorCounter++; // Increment counter for next element return `<${tagName} data-ts-sel="${selector}"${attributes}>`; } return match; }); return processedHtml; } /** * Creates a short hash from a longer string (like UUID) * Uses base36 encoding for maximum compactness while staying readable */ function createShortHash(input) { let hash = 0; for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } // Convert to base36 and take first 8 characters, ensure positive return Math.abs(hash).toString(36).substring(0, 8).padStart(8, '0'); } /** * Calculates size savings from using compact format */ function calculateSavings(verbose, compact) { const verboseSize = JSON.stringify(verbose).length; const compactSize = compact.length; const savings = verboseSize - compactSize; const percentage = Math.round((savings / verboseSize) * 100); return { verboseSize, compactSize, savings, percentage }; } //# sourceMappingURL=utils.js.map