Files
vendoo/lib/html-templates.mjs

164 lines
10 KiB
JavaScript

const TEMPLATES = {
vinted: {
name: 'Vinted Classic',
wrap(listing) {
const tags = Array.isArray(listing.tags) ? listing.tags.join(' ') : listing.tags || '';
return `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;color:#333">
<p style="font-size:15px;line-height:1.7;margin:0 0 16px">${richDescription(listing)}</p>
${listing.brand || listing.size || listing.color ? `<table style="border-collapse:collapse;width:100%;margin:16px 0;font-size:14px">
${listing.brand ? `<tr><td style="padding:8px 12px;background:#f8f7f4;border:1px solid #e8e6e0;font-weight:600;width:130px">Marke</td><td style="padding:8px 12px;border:1px solid #e8e6e0">${esc(listing.brand)}</td></tr>` : ''}
${listing.category ? `<tr><td style="padding:8px 12px;background:#f8f7f4;border:1px solid #e8e6e0;font-weight:600">Kategorie</td><td style="padding:8px 12px;border:1px solid #e8e6e0">${esc(listing.category)}</td></tr>` : ''}
${listing.size ? `<tr><td style="padding:8px 12px;background:#f8f7f4;border:1px solid #e8e6e0;font-weight:600">Größe</td><td style="padding:8px 12px;border:1px solid #e8e6e0">${esc(listing.size)}</td></tr>` : ''}
${listing.color ? `<tr><td style="padding:8px 12px;background:#f8f7f4;border:1px solid #e8e6e0;font-weight:600">Farbe</td><td style="padding:8px 12px;border:1px solid #e8e6e0">${esc(listing.color)}</td></tr>` : ''}
${listing.condition ? `<tr><td style="padding:8px 12px;background:#f8f7f4;border:1px solid #e8e6e0;font-weight:600">Zustand</td><td style="padding:8px 12px;border:1px solid #e8e6e0">${esc(listing.condition)}</td></tr>` : ''}
</table>` : ''}
${tags ? `<p style="color:#6c5ce7;font-size:14px;margin:12px 0 0">${esc(tags)}</p>` : ''}
</div>`;
},
},
'ebay-ka': {
name: 'Kleinanzeigen Modern',
wrap(listing) {
return `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:600px;color:#333">
<p style="font-size:15px;line-height:1.7;margin:0 0 16px">${richDescription(listing)}</p>
${listing.brand || listing.size ? `<div style="background:linear-gradient(135deg,#f0f7f4,#f6f5f0);border-radius:10px;padding:16px 20px;margin:16px 0">
<p style="font-weight:700;font-size:13px;text-transform:uppercase;letter-spacing:0.5px;color:#888;margin:0 0 10px">Artikeldetails</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:14px">
${listing.brand ? `<div><span style="color:#888">Marke:</span> <strong>${esc(listing.brand)}</strong></div>` : ''}
${listing.category ? `<div><span style="color:#888">Kategorie:</span> <strong>${esc(listing.category)}</strong></div>` : ''}
${listing.size ? `<div><span style="color:#888">Größe:</span> <strong>${esc(listing.size)}</strong></div>` : ''}
${listing.color ? `<div><span style="color:#888">Farbe:</span> <strong>${esc(listing.color)}</strong></div>` : ''}
${listing.condition ? `<div><span style="color:#888">Zustand:</span> <strong>${esc(listing.condition)}</strong></div>` : ''}
</div>
</div>` : ''}
</div>`;
},
},
'ebay-de': {
name: 'eBay Professional',
wrap(listing) {
return `<div style="font-family:system-ui,-apple-system,sans-serif;max-width:700px;color:#333">
<div style="border-bottom:3px solid #e53238;padding-bottom:12px;margin-bottom:16px">
<h2 style="margin:0;font-size:20px;color:#333">${esc(listing.title)}</h2>
</div>
<p style="font-size:15px;line-height:1.8;margin:0 0 20px">${richDescription(listing)}</p>
${listing.brand || listing.size ? `<div style="background:#f7f7f7;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;margin:20px 0">
<div style="background:#333;color:#fff;padding:10px 16px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Artikelmerkmale</div>
<table style="width:100%;border-collapse:collapse;font-size:14px">
${listing.brand ? `<tr><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8;font-weight:600;width:140px;background:#fafafa">Marke</td><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8">${esc(listing.brand)}</td></tr>` : ''}
${listing.category ? `<tr><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8;font-weight:600;background:#fafafa">Typ</td><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8">${esc(listing.category)}</td></tr>` : ''}
${listing.size ? `<tr><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8;font-weight:600;background:#fafafa">Größe</td><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8">${esc(listing.size)}</td></tr>` : ''}
${listing.color ? `<tr><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8;font-weight:600;background:#fafafa">Farbe</td><td style="padding:10px 16px;border-bottom:1px solid #e8e8e8">${esc(listing.color)}</td></tr>` : ''}
${listing.condition ? `<tr><td style="padding:10px 16px;font-weight:600;background:#fafafa">Zustand</td><td style="padding:10px 16px">${esc(listing.condition)}</td></tr>` : ''}
</table>
</div>` : ''}
<p style="font-size:12px;color:#999;margin:16px 0 0;border-top:1px solid #eee;padding-top:12px">Privatverkauf — keine Garantie, keine Rücknahme.</p>
</div>`;
},
},
etsy: {
name: 'Etsy Handmade',
wrap(listing) {
const tags = Array.isArray(listing.tags) ? listing.tags : [];
return `<div style="font-family:'Georgia',serif;max-width:600px;color:#333">
<p style="font-size:15px;line-height:1.8;margin:0 0 20px">${richDescription(listing)}</p>
${listing.brand || listing.size ? `<div style="border-top:2px solid #f1641e;border-bottom:2px solid #f1641e;padding:16px 0;margin:20px 0">
<p style="font-family:system-ui,sans-serif;font-weight:700;font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#f1641e;margin:0 0 12px">Details</p>
<div style="font-family:system-ui,sans-serif;font-size:14px;line-height:2">
${listing.brand ? `<div>✦ <strong>Brand:</strong> ${esc(listing.brand)}</div>` : ''}
${listing.category ? `<div>✦ <strong>Category:</strong> ${esc(listing.category)}</div>` : ''}
${listing.size ? `<div>✦ <strong>Size:</strong> ${esc(listing.size)}</div>` : ''}
${listing.color ? `<div>✦ <strong>Color:</strong> ${esc(listing.color)}</div>` : ''}
${listing.condition ? `<div>✦ <strong>Condition:</strong> ${esc(listing.condition)}</div>` : ''}
</div>
</div>` : ''}
${tags.length ? `<p style="font-family:system-ui,sans-serif;font-size:13px;color:#888;margin:12px 0 0">${tags.map(t => `<span style="display:inline-block;background:#fdf0e0;color:#8a5a1e;padding:3px 10px;border-radius:12px;margin:2px 3px;font-size:12px">${esc(t)}</span>`).join(' ')}</p>` : ''}
</div>`;
},
},
};
function esc(s) { return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function nl2br(s) { return esc(s || '').replace(/\n/g, '<br>'); }
const ALLOWED_RICH_TAGS = new Set([
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'strike', 'ol', 'ul', 'li',
'blockquote', 'a', 'h1', 'h2', 'h3', 'span', 'div', 'sub', 'sup', 'code', 'pre',
]);
const VOID_RICH_TAGS = new Set(['br']);
function sanitizeStyle(style) {
const safe = [];
for (const declaration of String(style || '').split(';')) {
const [rawName, ...rest] = declaration.split(':');
const name = String(rawName || '').trim().toLowerCase();
const value = rest.join(':').trim();
if (!value) continue;
if (!['color', 'background-color', 'text-align'].includes(name)) continue;
if (/url\s*\(|expression\s*\(|javascript:/i.test(value)) continue;
if (name === 'text-align' && !/^(left|right|center|justify)$/i.test(value)) continue;
if (['color', 'background-color'].includes(name) && !/^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%]+\)|hsla?\([\d\s.,%]+\)|[a-z]{3,20})$/i.test(value)) continue;
safe.push(`${name}:${value}`);
}
return safe.join(';');
}
export function sanitizeRichHtml(input) {
let html = String(input || '');
html = html
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<(script|style|iframe|object|embed|form|input|button|textarea|select|option|svg|math|video|audio)[^>]*>[\s\S]*?<\/\1\s*>/gi, '')
.replace(/<(script|style|iframe|object|embed|form|input|button|textarea|select|option|svg|math|video|audio)\b[^>]*\/?\s*>/gi, '');
return html.replace(/<\/?([a-z0-9-]+)([^>]*)>/gi, (full, rawTag, rawAttrs) => {
const tag = String(rawTag || '').toLowerCase();
const closing = /^<\//.test(full);
if (!ALLOWED_RICH_TAGS.has(tag)) return '';
if (closing) return VOID_RICH_TAGS.has(tag) ? '' : `</${tag}>`;
const attrs = [];
const source = String(rawAttrs || '');
source.replace(/([a-z0-9:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi, (_m, rawName, dq, sq, bare) => {
const name = String(rawName || '').toLowerCase();
const value = dq ?? sq ?? bare ?? '';
if (name.startsWith('on')) return '';
if (name === 'href' && tag === 'a') {
const href = String(value).trim();
if (/^(https?:|mailto:|tel:|#|\/)/i.test(href)) attrs.push(`href="${esc(href)}"`);
} else if (name === 'target' && tag === 'a' && ['_blank', '_self'].includes(value)) {
attrs.push(`target="${value}"`);
} else if (name === 'rel' && tag === 'a') {
attrs.push('rel="noopener noreferrer"');
} else if (name === 'style') {
const safeStyle = sanitizeStyle(value);
if (safeStyle) attrs.push(`style="${esc(safeStyle)}"`);
} else if (name === 'class' && /^ql-(align|indent|size|font|color|background)-[a-z0-9-]+$/i.test(value)) {
attrs.push(`class="${esc(value)}"`);
}
return '';
});
if (tag === 'a' && attrs.some(attr => attr.startsWith('target="_blank"')) && !attrs.some(attr => attr.startsWith('rel='))) {
attrs.push('rel="noopener noreferrer"');
}
return `<${tag}${attrs.length ? ` ${attrs.join(' ')}` : ''}>`;
});
}
function richDescription(listing) {
const html = sanitizeRichHtml(listing?.description_html || '');
return html || nl2br(listing?.description || '');
}
export function wrapInTemplate(listing, platformId) {
const tpl = TEMPLATES[platformId];
if (!tpl) return `<p>${richDescription(listing)}</p>`;
return tpl.wrap(listing);
}
export function getHtmlTemplates() {
return Object.entries(TEMPLATES).map(([id, t]) => ({ id, name: t.name }));
}