* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
164 lines
10 KiB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
|
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 }));
|
|
}
|