/** * Adapter SIC (sic.com.pl) — parser strony kolekcji tkaniny. * Wejście: utrwalony snapshot HTML strony kolekcji (np. /oferta/.../arena/). * Wyjście: nazwa kolekcji, lista odcieni z URL pełnowymiarowych próbek, * link do karty PDF (product-profile). Bez sieci, bez zapisu do DB. */ export class SicParseError extends Error { constructor(public readonly code: string, message: string) { super(message); this.name = "SicParseError"; } } export interface SicShade { /** kod w konwencji Vilmax — dwucyfrowy ("01", "23") */ code: string; /** numer z nazwy pliku u producenta */ vendorCode: string; name: string; swatchUrl: string; } export interface SicCollection { collection: string; producer: string; shades: SicShade[]; cardUrl: string | null; } // Pełnowymiarowa próbka: --.jpg — warianty srcset // kończą się na -WxH i są odfiltrowane osobno. const SWATCH_RE = /https:\/\/(?:www\.)?sic\.com\.pl\/wp-content\/uploads\/\d{4}\/\d{2}\/([A-Za-z][A-Za-z0-9]*)-(\d{1,3})-([A-Za-z][A-Za-z0-9-]*?)\.(?:jpe?g|png|webp)(?:\?[^\s"']*)?/gi; export function parseSicCollectionPage(html: string, pageUrl: string): SicCollection { const host = new URL(pageUrl).hostname; if (!/(^|\.)sic\.com\.pl$/i.test(host)) { throw new SicParseError("unsupported_host", "Adapter SIC obsługuje wyłącznie sic.com.pl."); } const byCode = new Map(); const prefixes = new Map(); for (const m of html.matchAll(SWATCH_RE)) { const [, collection, num, rawName] = m; if (/x\d+$/i.test(rawName!)) continue; // warianty rozmiarowe -448x448 itd. const name = rawName!.replace(/-/g, " ").trim(); if (!name) continue; const collKey = collection!.toLowerCase(); prefixes.set(collKey, (prefixes.get(collKey) ?? 0) + 1); const key = `${collKey}:${String(Number(num)).padStart(2, "0")}`; if (!byCode.has(key)) { byCode.set(key, { code: String(Number(num)).padStart(2, "0"), vendorCode: num!, name, swatchUrl: m[0], }); } } if (!byCode.size) { throw new SicParseError("no_shades", "Strona nie zawiera odcieni w konwencji SIC (--.jpg)."); } // Kolekcja = dominujący prefiks nazw plików próbek (na stronie bywają // próbki innych kolekcji — banery, powiązane produkty). const collection = [...prefixes.entries()].sort((a, b) => b[1] - a[1])[0]![0]; const shades = [...byCode.entries()] .filter(([key]) => key.startsWith(`${collection}:`)) .map(([, s]) => s) .sort((a, b) => a.code.localeCompare(b.code)); if (!shades.length) throw new SicParseError("no_shades", "Nie udało się wyodrębnić odcieni kolekcji."); // Karta PDF: -product-profile.pdf (dowolna wielkość liter). const cardRe = new RegExp( `https:\\/\\/(?:www\\.)?sic\\.com\\.pl\\/wp-content\\/uploads\\/[^\\s"']*?${collection}-product-profile\\.pdf(?:\\?[^\\s"']*)?`, "i", ); const cardUrl = html.match(cardRe)?.[0] ?? null; const collectionName = collection.charAt(0).toUpperCase() + collection.slice(1); return { collection: collectionName, producer: "SIC", shades, cardUrl }; }