UNPKG

libreria-astro-lefebvre

Version:

Librería de componentes Astro, React y Vue para Lefebvre

363 lines (311 loc) 12 kB
import { components } from '../generated/componentRegistry.ts'; // ============================================================================ // 🎯 Grafo JSON-LD // ============================================================================ // @id RELATIVO de fragmento del nodo principal de la página (WebPage/CollectionPage…), // que emite SEO_Schema_Page. Es la fuente única de la convención: cualquier bloque del mismo // documento (incluido el HTML inyectado por el render API del CMS) puede colgar de la página con // isPartOf: { "@id": PAGE_ENTITY_ID } sin necesidad de conocer la URL/canonical. export const PAGE_ENTITY_ID = '#webpage'; // @id RELATIVO del ItemList principal de una página. Convención: el listado principal lleva este @id // y la CollectionPage (vía SEO_Schema_Page mainEntityId) lo referencia como mainEntity. Relativo → // resuelve contra la página, así que vale tanto para el front como para el render API del CMS. export const MAIN_ITEMLIST_ID = '#itemlist'; // ============================================================================ // 🎯 Utilidades de Imagen para Limbo // ============================================================================ // NOTA: Estas funciones están copiadas de component-limbo/src/utils/helpers.js // para evitar problemas de dependencias en entornos sin acceso directo a limbo-component. // Si se actualizan en helpers.js, sincronizar aquí también. // ============================================================================ /** * URLs base de Limbo según entorno */ export const LIMBO_BASE_URL = { DEV: 'https://led-dev-limbo-dev.eu.els.local', PROD: 'https://limbo.lefebvre.es' }; /** * URLs base de LF2 según entorno */ export function resolveLf2ResourceUrl(isProd = false) { return isProd ? 'https://lf2.lefebvre.es/js/leadform-api.js' : 'https://led-dev-leadformv2-dev08.eu.els.local/js/leadform-api.js'; } /** * Convierte URLs relativas de Limbo a absolutas */ export function resolveUrl(url, isProd = false) { if (!url) return ''; if (url.startsWith('/files/')) { const baseUrl = isProd ? LIMBO_BASE_URL.PROD : LIMBO_BASE_URL.DEV; return baseUrl + url; } return url; } /** * Decodifica entidades HTML comunes en un string */ export function decodeHtmlEntities(value) { if (typeof value !== 'string') return value; return value .replace(/&quot;/g, '"') .replace(/&amp;/g, '&') .replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&#39;/g, "'") .replace(/&#x27;/g, "'"); } /** * Verifica si una URL es válida y usable (no blob, no vacía) */ export function isValidImageUrl(url) { if (!url || typeof url !== 'string') return false; if (url.startsWith('blob:')) return false; return true; } /** * 🎯 FUNCIÓN PRINCIPAL - Extrae URL de imagen de cualquier formato * * @param {string} value - URL directa o JSON de Limbo * @param {Object} options - { prefer: 'crop'|'original', isProd: boolean } * @returns {string} URL lista para usar en <img src=""> */ export function extractImageUrl(value, options = {}) { if (!value) return ''; const { prefer = 'crop', isProd = false } = options; const normalizedValue = decodeHtmlEntities(value); try { const data = JSON.parse(normalizedValue); const findValidCropUrl = () => { if (data.images && Array.isArray(data.images)) { for (const img of data.images) { if (img && isValidImageUrl(img.url)) { return img.url; } } } return null; }; const originalUrl = data.original?.url; const cropUrl = findValidCropUrl(); if (prefer === 'crop') { if (cropUrl) return resolveUrl(cropUrl, isProd); if (isValidImageUrl(originalUrl)) return resolveUrl(originalUrl, isProd); } else { if (isValidImageUrl(originalUrl)) return resolveUrl(originalUrl, isProd); if (cropUrl) return resolveUrl(cropUrl, isProd); } if (isValidImageUrl(data.url)) { return resolveUrl(data.url, isProd); } return ''; } catch { if (typeof normalizedValue === 'string') { if (normalizedValue.startsWith('blob:')) return ''; if (normalizedValue.startsWith('/files/')) return resolveUrl(normalizedValue, isProd); if (normalizedValue.startsWith('http') || normalizedValue.startsWith('/')) return normalizedValue; } } if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) { return ''; } return value; } /** * Obtiene datos completos de imagen (original + recortes) */ export function parseImageData(value, options = {}) { const { isProd = false } = options; if (!value) return { original: null, images: [], url: '' }; try { const data = JSON.parse(decodeHtmlEntities(value)); const resolvedOriginal = data.original ? { ...data.original, url: resolveUrl(data.original.url, isProd) } : null; const resolvedImages = (data.images || []).map(img => ({ ...img, url: resolveUrl(img.url, isProd) })); return { original: resolvedOriginal, images: resolvedImages, url: resolvedImages[0]?.url || resolvedOriginal?.url || resolveUrl(data.url, isProd) || '' }; } catch { return { original: null, images: [], url: resolveUrl(value, isProd) }; } } // ============================================================================ // 🎯 Funciones para múltiples recortes (Multi-crop support) // ============================================================================ /** * Extrae todos los recortes de una imagen en formato key-value * * Soporta dos formatos de entrada: * 1. JSON de Limbo (desde PageBuilder): * {"original":{"url":"..."}, "images":[{"name":"base","url":"..."},{"name":"sky","url":"..."}]} * 2. Objeto simple (uso estático sin PageBuilder): * { base: "https://...", skyscraper: "https://..." } * * @param {string|object} value - JSON de Limbo, URL directa, u objeto {cropName: url} * @param {Object} options - { isProd: boolean, includeOriginal: boolean } * @returns {Object} Objeto con crops: { base: "url", skyscraper: "url", original?: "url" } * * @example * // Desde PageBuilder (JSON de Limbo) * const crops = extractAllCrops(image); * const baseUrl = crops.base; * const skyUrl = crops.skyscraper; * * @example * // Uso estático (objeto simple) * <Componente image={{ base: "url1", skyscraper: "url2" }} /> * const crops = extractAllCrops(image); // Devuelve { base: "url1", skyscraper: "url2" } */ export function extractAllCrops(value, options = {}) { const { isProd = false, includeOriginal = true } = options; if (!value) return {}; // Caso 1: Ya es un objeto simple {key: url} - uso estático sin PageBuilder if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Verificar si es un objeto simple (todas las propiedades son strings URL) const keys = Object.keys(value); const isSimpleObject = keys.length > 0 && keys.every(key => typeof value[key] === 'string' || value[key] === null || value[key] === undefined ); if (isSimpleObject) { // Resolver URLs relativas si es necesario const result = {}; for (const key of keys) { if (value[key]) { result[key] = resolveUrl(value[key], isProd); } } return result; } } // Caso 2: Es un string - puede ser JSON de Limbo o URL directa if (typeof value === 'string') { const normalizedValue = decodeHtmlEntities(value); try { const data = JSON.parse(normalizedValue); const result = {}; // Extraer todos los crops del array images if (data.images && Array.isArray(data.images)) { for (const img of data.images) { if (img && img.name && isValidImageUrl(img.url)) { result[img.name] = resolveUrl(img.url, isProd); } } } // Incluir original si se solicita if (includeOriginal && data.original?.url && isValidImageUrl(data.original.url)) { result.original = resolveUrl(data.original.url, isProd); } // Si no hay crops pero hay URL directa en el JSON if (Object.keys(result).length === 0 && data.url && isValidImageUrl(data.url)) { result.default = resolveUrl(data.url, isProd); } return result; } catch { // No es JSON válido - tratar como URL directa if (isValidImageUrl(normalizedValue)) { return { default: resolveUrl(normalizedValue, isProd) }; } } } return {}; } /** * Crea el objeto de props de imagen para uso estático (sin PageBuilder) * * Útil cuando se usa un componente directamente desde la librería * y se quiere pasar múltiples crops de forma estructurada. * * @param {Object} crops - Objeto con los crops: { base: "url", skyscraper: "url" } * @param {string} originalUrl - URL de la imagen original (opcional) * @returns {string} JSON string compatible con el formato de Limbo * * @example * // En un archivo .astro * import { createImageProps } from 'libreria-astro-lefebvre/lib/functions'; * * <Componente * image={createImageProps({ * base: "https://example.com/base.jpg", * skyscraper: "https://example.com/sky.jpg" * })} * /> * * // También se puede pasar el objeto directamente (más simple): * <Componente image={{ base: "url1", skyscraper: "url2" }} /> */ export function createImageProps(crops, originalUrl = null) { if (!crops || typeof crops !== 'object') return ''; const images = Object.entries(crops).map(([name, url]) => ({ name, url, width: null, height: null })); const result = { original: originalUrl ? { url: originalUrl } : null, images }; return JSON.stringify(result); } /** * Obtiene un crop específico por nombre, con fallback * * @param {string|object} value - JSON de Limbo u objeto simple * @param {string} cropName - Nombre del crop a buscar * @param {Object} options - { isProd: boolean, fallback: string } * @returns {string} URL del crop o fallback * * @example * const baseUrl = getCropByName(image, 'base'); * const skyUrl = getCropByName(image, 'skyscraper', { fallback: baseUrl }); */ export function getCropByName(value, cropName, options = {}) { const { isProd = false, fallback = '' } = options; const allCrops = extractAllCrops(value, { isProd }); return allCrops[cropName] || allCrops.original || allCrops.default || fallback; } /** * Preprocesa campos de imagen para enviar a servidor de preview */ export function prepareImageFieldsForPreview(obj, options = {}) { const { isProd = false } = options; if (!obj?.fields) return obj; const processedFields = obj.fields.map(field => { if (field.type === 'image' && typeof field.example_value === 'string') { const extractedUrl = extractImageUrl(field.example_value, { isProd }); return { ...field, example_value: extractedUrl || field.example_value }; } return field; }); return { ...obj, fields: processedFields }; } export function listComponents() { const metadatas = components .map(c => c.component.metadata) .sort((a, b) => { const priorityA = a.priority ?? 0; const priorityB = b.priority ?? 0; if (priorityA !== priorityB) { return priorityB - priorityA; // Higher priority first } return (a.name || '').localeCompare(b.name || ''); // Alphabetically by name }); return metadatas; }