@magnusbag/livets-core
Version:
TypeScript API layer for LiveTS framework
252 lines • 7.83 kB
JavaScript
;
/**
* 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 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