libreria-astro-lefebvre
Version:
Librería de componentes Astro, React y Vue para Lefebvre
363 lines (311 loc) • 12 kB
JavaScript
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(/"/g, '"')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, "'")
.replace(/'/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;
}