;try { let widget = '.widget-type_widget_v4_product_info_2_5f6f3f54bc3450ac4a98b2a8fb5a2a3b'; let $widget = $(widget); $(function() { // --------------------------------------------------- // Tabs (как было) // --------------------------------------------------- $widget.find("[data-tabs-item]").on("click", function() { let this_tab_head = $(this); let tabs = $(this).closest(".tabs"); let tab_item_id = $(this).data("tabsItem"); let open_tab = tabs.find('#' + tab_item_id); if (open_tab.length) { if (this_tab_head.parents(".tabs__content").length && this_tab_head.is(".is-active")) { this_tab_head.removeClass("is-active"); tabs.find('#' + tab_item_id).addClass("is-hide-mobile"); } else { tabs.find(".tabs__item.is-active, .tabs__head-item.is-active").removeClass("is-active"); tabs.find('#' + tab_item_id).addClass("is-active").removeClass("is-hide-mobile"); this_tab_head.addClass("is-active"); tabs.find('[data-tabs-item="' + tab_item_id + '"]').addClass("is-active"); $('html, body').animate({ scrollTop: this_tab_head.offset().top - 5 }, 'slow'); } if (open_tab.find(".masonry-reviews-list").length) { resizeAllMassonryGridItems(); } } }); // --------------------------------------------------- // ✅ SEO STRUCTURED DESCRIPTION (FIXED DISTRIBUTION) // --------------------------------------------------- (function() { const desc = document.querySelector(widget + " .product-description.static-text"); if (!desc) return; const rawOriginal = (desc.innerText || "").replace(/\u00A0/g, " ").trim(); if (!rawOriginal || rawOriginal.length < 10) return; if (desc.querySelector(".seo-structured-desc")) return; const norm = (s) => String(s || "").replace(/\s+/g, " ").trim(); const lower = (s) => norm(s).toLowerCase(); const uniq = (arr) => Array.from(new Set((arr || []).filter(Boolean))); // ------------------------------- // 1) FIX "one word per line" // ------------------------------- function normalizeBrokenLines(text) { let t = String(text || ""); t = t.replace(/\r/g, "\n"); const joinPairs = [ ["Utility\nnumber", "Utility numbers"], ["Utility\nnumbers", "Utility numbers"], ["OE\nnumbers", "OE numbers"], ["Parts\nDescription", "Parts Description"], ["Dealer\npart\nnumber", "Dealer part number"], ["Part\nNumber", "Part Number"], ["Parts\nstate", "Parts state"], ["Packing\nunit", "Packing unit"], ["Quantity\nper\nPU", "Quantity per PU"], ["Fitting\nPosition", "Fitting Position"], ["Properties\nvehicle", "Properties vehicle"], ["Constr.\nyear", "Constr.year"], ["Engine\nCode", "Engine Code"], ["KBA\nKeynumber", "KBA Keynumber"], ["Brake\nSystem", "Brake System"], ["Supplementary\nArticle", "Supplementary Article"], ["Observe\nvehicle\nspecifications", "Observe the vehicle manufacturer specifications"], ["Replacement\nnumbers", "Replacement numbers"], ["General\nInformation", "General Information"], ["Additional\ninformation", "Additional information"], ]; joinPairs.forEach(([a,b]) => { t = t.replace(new RegExp(a, "gi"), b); }); t = t.replace(/\n{3,}/g, "\n\n"); return t.trim(); } const raw = normalizeBrokenLines(rawOriginal); let lines = raw.split(/\n/).map(s => norm(s)).filter(Boolean); // ------------------------------- // 2) SRB/HR -> EN keys (как раньше) // ------------------------------- const mapLine = (ln) => { let l = ln; l = l.replace(/\bTezina\b/gi, "Weight"); l = l.replace(/\bVisina\b/gi, "Height"); l = l.replace(/\bDuzina\b/gi, "Length"); l = l.replace(/\bSirina\b/gi, "Width"); l = l.replace(/\bMaterijal\b/gi, "Material"); l = l.replace(/\bStrana\s+ugradnje\b/gi, "Fitting Position"); l = l.replace(/\bPrednja\s+osovina\b/gi, "Front Axle"); l = l.replace(/\bZadnja\s+osovina\b/gi, "Rear Axle"); l = l.replace(/\bPrednja\b/gi, "Front"); l = l.replace(/\bZadnja\b/gi, "Rear"); return norm(l); }; lines = lines.map(mapLine); // ------------------------------- // 3) SECTION FINDERS // ------------------------------- const HEADER_RE = /^(description|dealer part number|part number|parts description|packing unit|quantity per pu|parts state|ean|vehicle|constr\.year|power|capacity|engine code|kba keynumber|properties vehicle|characteristics|utility numbers|oe numbers|replacement numbers|additional information|general information|images|page\b|from\b)/i; function findLineIndexStartsWith(label) { const l = label.toLowerCase(); for (let i = 0; i < lines.length; i++) { if (lower(lines[i]).startsWith(l)) return i; } return -1; } function collectUntilNextHeader(startIdx) { if (startIdx < 0) return []; let out = []; for (let i = startIdx + 1; i < lines.length; i++) { if (HEADER_RE.test(lines[i])) break; out.push(lines[i]); } return out; } function getValueAfterPrefix(prefix) { const p = prefix.toLowerCase(); for (const ln of lines) { const ll = ln.toLowerCase(); if (ll.startsWith(p)) return norm(ln.slice(prefix.length)); } return ""; } // ------------------------------- // 4) BASE FIELDS // ------------------------------- // EAN: берем первый 13-значный, остальные как "other numbers" const all13 = uniq(raw.match(/\b\d{13}\b/g) || []); const ean = all13[0] || ""; const other13 = all13.slice(1); const dealerPartNumber = getValueAfterPrefix("Dealer part number"); const partNumber = getValueAfterPrefix("Part Number"); const partsDesc = getValueAfterPrefix("Parts Description"); const packingUnit = getValueAfterPrefix("Packing unit"); const qtyPerPU = getValueAfterPrefix("Quantity per PU"); const partsState = getValueAfterPrefix("Parts state"); // Vehicle section: лучше собирать как секцию const idxVehicle = findLineIndexStartsWith("Vehicle"); const vehicleLines = idxVehicle >= 0 ? [lines[idxVehicle], ...collectUntilNextHeader(idxVehicle)] : []; const vehicleLine = getValueAfterPrefix("Vehicle"); const constrYear = getValueAfterPrefix("Constr.year"); const power = getValueAfterPrefix("Power"); const capacity = getValueAfterPrefix("Capacity"); const engineCode = getValueAfterPrefix("Engine Code"); const kba = getValueAfterPrefix("KBA Keynumber"); // ------------------------------- // 5) OE NUMBERS (FIX: только OE-форматы) // ------------------------------- const idxOE = findLineIndexStartsWith("OE numbers"); const oeLines = idxOE >= 0 ? collectUntilNextHeader(idxOE) : []; // Mercedes style: A651 140 00 60 / 651 140 00 60 const reMercedesOE = /\bA?\d{3}\s?\d{3}\s?\d{2}\s?\d{2}\b/g; // VAG/PSA style: 1K0 698 451 D / JZW 698 451 D / 8P0 098 601 M const reGroupOE = /\b[A-Z0-9]{1,4}\s?\d{3}\s?\d{3}\s?\d{3}\s?[A-Z]?\b/g; // Compact OE: A6511400060 / 1610489680 const reCompactOE = /\bA?\d{9,11}\b/g; const oeText = oeLines.join(" "); const oeFound = uniq([ ...(oeText.match(reMercedesOE) || []), ...(oeText.match(reGroupOE) || []), ...(oeText.match(reCompactOE) || []), ].map(norm)); // Brand prefix line like "Mercedes-Benz: A651 140 00 60" // вытаскиваем такие тоже, но чистим до номера const brandPref = uniq( oeLines .map(l => { const m = l.match(/^[A-Za-z\- ]+:\s*(.+)$/); return m ? m[1] : ""; }) .filter(Boolean) .flatMap(v => (v.match(reMercedesOE) || []).concat(v.match(reGroupOE) || []).concat(v.match(reCompactOE) || [])) .map(norm) ); const oeNumbers = uniq([...oeFound, ...brandPref]) .filter(x => x && x !== ean); // ------------------------------- // 6) Utility numbers (чистим мусор) // ------------------------------- const idxUtil = findLineIndexStartsWith("Utility numbers"); const utilLines = idxUtil >= 0 ? collectUntilNextHeader(idxUtil) : []; let utilTokens = utilLines.join(", ").split(",").map(norm).filter(Boolean); const isGarbage = (t) => { const tl = lower(t); if (!t) return true; if (tl === "-" || tl === "—") return true; if (tl.startsWith("page")) return true; if (tl === "from") return true; if (tl.startsWith("images")) return true; if (/\b\d{2}\.\d{2}\.\d{4}\b/.test(tl)) return true; if (/^\d+$/.test(tl) && tl.length <= 2) return true; // длинные "CIAK - ... - ..." if (t.includes(" - ") && /^.{2,}\s-\s.{2,}\s-\s/.test(t)) return true; return false; }; const utilityNumbers = uniq(utilTokens.filter(t => !isGarbage(t))); // ------------------------------- // 7) Replacement numbers (Новый блок) // ------------------------------- const idxRepl = findLineIndexStartsWith("Replacement numbers"); const replLines = idxRepl >= 0 ? collectUntilNextHeader(idxRepl) : []; const replTokens = uniq( replLines .join(" ") .split(/[,\s]+/) .map(norm) .filter(Boolean) .filter(t => !isGarbage(t)) ); // ------------------------------- // 8) Characteristics section (строками) // ------------------------------- const idxChars = findLineIndexStartsWith("Characteristics"); const charsLines = idxChars >= 0 ? collectUntilNextHeader(idxChars) : []; const charsClean = charsLines.filter(l => !isGarbage(l)); // ------------------------------- // 9) Additional information / General information (как отдельный блок) // ------------------------------- const idxAdd = findLineIndexStartsWith("Additional information"); const addLines = idxAdd >= 0 ? collectUntilNextHeader(idxAdd) : []; const idxGen = findLineIndexStartsWith("General Information"); const genLines = idxGen >= 0 ? collectUntilNextHeader(idxGen) : []; const infoLines = uniq([...addLines, ...genLines].map(norm)).filter(Boolean); // ------------------------------- // 10) Sizes / Tech (mm, kg, kW, HP, dB(A), inch…) // ------------------------------- const tech = uniq(raw.match(/\b\d+(?:[.,]\d+)?\s?(mm|cm|m|bar|v|volt|kg|g|nm|a|w|kw|hp|db\(a\)|inch|")\b/gi) || []) .map(norm); // ------------------------------- // 11) Fitment tokens (Front/Rear/Left/Right/Inner/Outer...) // ------------------------------- let placeTokens = []; const rawL = raw.toLowerCase(); if (rawL.includes("front axle")) placeTokens.push("Front Axle"); if (rawL.includes("rear axle")) placeTokens.push("Rear Axle"); const posMap = [ ["fitting position front", "Front"], ["fitting position rear", "Rear"], ["front", "Front"], ["rear", "Rear"], ["left", "Left"], ["right", "Right"], ["upper", "Upper"], ["lower", "Lower"], ["inner", "Inner"], ["outer", "Outer"] ]; posMap.forEach(([k,v]) => { if (rawL.includes(k)) placeTokens.push(v); }); placeTokens = uniq(placeTokens); // ------------------------------- // 12) ✅ ANALOGS/CROSSES (FIXED) // Берем ТОЛЬКО коды/номера, без слов // ------------------------------- const STOP_WORDS = new Set([ "dealer","part","number","numbers","packing","quantity","parts","state","manufacturer", "vehicle","constr.year","power","capacity","engine","code","keynumber","characteristics", "operating","electric","supplementary","article","without","gasket","recommended","accessories", "diameter","observe","specifications","control","software","trained","updated","always","compare", "replacement","replaced","additional","information","general","components","electrostatic","discharge", "contacts","valve","page","from","images" ]); // "код" = содержит цифру И (есть дефис/точка/или буквы+цифры), длина >= 5 function isCodeToken(t) { const s = norm(t).replace(/[.,;:]+$/g, ""); if (!s) return false; const sl = s.toLowerCase(); if (STOP_WORDS.has(sl)) return false; if (isGarbage(s)) return false; // не единицы if (/^(mm|cm|m|kg|g|kw|hp|v|w|nm|db\(a\)|inch|")$/i.test(s)) return false; // исключаем OE и основной EAN if (ean && s === ean) return false; if (oeNumbers.includes(s)) return false; // исключаем чисто "120kW" "163HP" — это тех данные if (/^\d+(?:[.,]\d+)?\s?(kw|hp|mm|cm|kg|g)\b/i.test(s)) return false; // основные паттерны кодов: // A2C59514268-VDO / E110059-MLS / LAK37-MAH / 83.1714A2 / 0 986 461 769 (последнее не поймаем как один, но ок) const compact = s.replace(/\s+/g, ""); if (/^[A-Z0-9]{2,}[A-Z0-9.\-]{3,}$/i.test(compact) && /\d/.test(compact)) return true; // 5+ цифр (utility) if (/^\d{5,}$/.test(compact)) return true; return false; } // собираем: // - dealerPartNumber / partNumber // - utilityNumbers // - replacement tokens // - другие коды из текста const tokensFromRaw = (raw.match(/[A-Z0-9][A-Z0-9.\-]{2,}/gi) || []).map(norm); const analogs = uniq([ dealerPartNumber, partNumber, ...utilityNumbers, ...replTokens, ...other13, // остальные 13-значные (как “другие номера”) ...tokensFromRaw ].filter(isCodeToken)); // отдельный блок “Замены” const replacementOut = uniq([...replTokens, ...other13].filter(isCodeToken)); // ------------------------------- // 13) OUTPUT HTML // ------------------------------- const esc = (s) => String(s || "") .replace(/&/g, "&").replace(//g, ">"); const listBlock = (arr) => "