// Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten). // Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen. export const CONSENT_KEY = 'wk-consent'; export const CONSENT_MAX_AGE = 183 * 86400000; export function vpClass(width) { const w = Number(width); if (w < 768) return 'm'; if (w < 1024) return 't'; return 'd'; } export function cleanPath(href) { let p = '/'; const h = String(href == null ? '' : href); if (h.charAt(0) === '/') { p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo' } else { try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ } } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } if (p.charAt(0) !== '/') p = '/' + p; return p.slice(0, 300); } const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, ''); export function refHost(referrer, ownHost) { if (!referrer) return ''; try { const u = new URL(String(referrer)); if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''; const h = u.hostname.toLowerCase(); if (!h || stripWww(h) === stripWww(ownHost || '')) return ''; return h; } catch (e) { return ''; } } // Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten). export function cssPath(el) { const parts = []; let n = el; while (n && n.tagName && parts.length < 6) { const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, ''); if (!tag) break; let idx = 1; for (let s = n.previousElementSibling; s; s = s.previousElementSibling) { if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++; } parts.unshift(tag + ':nth-of-type(' + idx + ')'); if (tag === 'body' || tag === 'html') break; n = n.parentNode; } let out = parts.join(' > '); while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); } return out.slice(0, 200); } function parseHref(a) { const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || ''; return String(href).trim(); } export function linkHost(a) { try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; } } export function contactKind(a, ownHost) { const href = parseHref(a); if (!href) return null; const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href); if (!scheme) return null; // relativ, Anker const s = scheme[1].toLowerCase(); if (s === 'tel') return 'tel'; if (s === 'mailto') return 'mail'; if (s === 'whatsapp') return 'wa'; if (s !== 'http' && s !== 'https') return null; const host = linkHost(a); if (!host) return null; if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa'; const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost; return stripWww(host) === stripWww(own || '') ? null : 'out'; } export function readConsent(storage, now) { try { const raw = storage.getItem(CONSENT_KEY); if (!raw) return null; const c = JSON.parse(raw); if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null; const age = now - c.t; if (age < 0 || age >= CONSENT_MAX_AGE) return null; return { v: 1, a: c.a, t: c.t }; } catch (e) { return null; } } export function contrastOk(hex) { if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false; const lin = (i) => { const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2); return 1.05 / (L + 0.05) >= 4.5; } export function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }