@linkitbe/sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,689 @@
1
+ (function() {
2
+
3
+ // ============================================================
4
+ // КОНФИГ (переопределяется через window.LinkItBeConfig)
5
+ // ============================================================
6
+
7
+ const config = Object.assign({
8
+ domain: window.location.hostname,
9
+ bootOnLoad: true,
10
+ contentSelector: null,
11
+ showPreview: true,
12
+ plainBlockJoiner: '\n',
13
+ plainBlockSelector: 'p, blockquote, h1, h2, h3, h4, h5, h6',
14
+ // null / не массив — все типы интеграций; иначе только перечисленные (например ['keyword', 'mini-offer']).
15
+ allowedFormats: ['keyword', 'mini-offer'],
16
+ apiBaseUrl: 'https://api.linkitbe.com',
17
+ partnerApiKey: null,
18
+ articleIdResolver: null,
19
+ // Автообработка бесконечной ленты: MutationObserver на новые узлы
20
+ enableFeedAutoProcess: false,
21
+ // Корень наблюдения для фида (обязателен при enableFeedAutoProcess=true).
22
+ feedObserverRootSelector: null,
23
+ sdkVersion: '1.0.2',
24
+ styles: {
25
+ link_color: '#e2187b',
26
+ link_weight: '700',
27
+ link_decoration: 'underline',
28
+ }
29
+ }, window.LinkItBeConfig || {});
30
+
31
+
32
+ // ============================================================
33
+ // 1. ПОЛУЧЕНИЕ ID СТАТЬИ
34
+ // ============================================================
35
+
36
+ function resolveArticleId(contentBlock) {
37
+ if (typeof config.articleIdResolver === 'function') {
38
+ try {
39
+ const fromResolver = config.articleIdResolver(contentBlock);
40
+ const value = fromResolver != null ? String(fromResolver).trim() : '';
41
+ if (value) return value;
42
+ } catch (e) {
43
+ console.warn('[LinkItBe] articleIdResolver failed', e);
44
+ }
45
+ }
46
+
47
+ return null;
48
+ }
49
+
50
+
51
+ // ============================================================
52
+ // 2. ЗАПРОС РАЗМЕТКИ
53
+ // ============================================================
54
+
55
+ async function fetchMarkup(articleId) {
56
+ const base = String(config.apiBaseUrl || 'https://api.linkitbe.com').replace(/\/$/, '');
57
+ const url = `${base}/v1/markup?article_id=${encodeURIComponent(articleId)}&domain=${encodeURIComponent(config.domain)}`;
58
+
59
+ const headers = { 'Accept': 'application/json' };
60
+ const partnerKey = config.partnerApiKey ? String(config.partnerApiKey).trim() : '';
61
+ if (partnerKey) {
62
+ headers['X-Partner-Api-Key'] = partnerKey;
63
+ }
64
+
65
+ const res = await fetch(url, {
66
+ method: 'GET',
67
+ headers,
68
+ credentials: 'omit',
69
+ });
70
+
71
+ if (!res.ok) return null;
72
+ return await res.json();
73
+ }
74
+
75
+ function getOrCreateSessionId() {
76
+ const key = '__lb_session_id';
77
+ try {
78
+ const existing = window.localStorage.getItem(key);
79
+ if (existing && existing.trim()) return existing.trim();
80
+ const generated = (window.crypto && window.crypto.randomUUID)
81
+ ? window.crypto.randomUUID()
82
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
83
+ window.localStorage.setItem(key, generated);
84
+ return generated;
85
+ } catch (e) {
86
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
87
+ }
88
+ }
89
+
90
+ function extractProcessingIdFromIntegrations(integrations) {
91
+ if (!Array.isArray(integrations)) return null;
92
+ for (const integration of integrations) {
93
+ const link = integration && integration.link ? String(integration.link) : '';
94
+ if (!link) continue;
95
+ try {
96
+ const u = new URL(link, window.location.origin);
97
+ const raw = u.searchParams.get('processing_id');
98
+ const value = Number(raw);
99
+ if (Number.isFinite(value) && value > 0) return value;
100
+ } catch (e) {
101
+ // ignore malformed link and continue
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function firstIntegrationWithProcessingId(integrations) {
108
+ if (!Array.isArray(integrations)) return null;
109
+ for (let i = 0; i < integrations.length; i++) {
110
+ const one = integrations[i];
111
+ if (one && extractProcessingIdFromIntegrations([one])) return one;
112
+ }
113
+ return null;
114
+ }
115
+
116
+ async function emitSdkEvent(eventType, path, articleId, integration, extra) {
117
+ const processingId = extractProcessingIdFromIntegrations([integration]);
118
+ if (!processingId) return;
119
+
120
+ const partnerKey = config.partnerApiKey ? String(config.partnerApiKey).trim() : '';
121
+ if (!partnerKey) return;
122
+
123
+ const payload = Object.assign({
124
+ event_id: (window.crypto && window.crypto.randomUUID)
125
+ ? window.crypto.randomUUID()
126
+ : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
127
+ event_type: eventType,
128
+ event_time: new Date().toISOString(),
129
+ article_id: articleId,
130
+ domain: config.domain,
131
+ processing_id: processingId,
132
+ placement_link: window.location.href,
133
+ integration_type: integration && integration.type != null ? integration.type : null,
134
+ sdk_version: String(config.sdkVersion || ''),
135
+ session_id: getOrCreateSessionId(),
136
+ referrer: document.referrer || null,
137
+ }, extra || {});
138
+
139
+ try {
140
+ const base = String(config.apiBaseUrl || 'https://api.linkitbe.com').replace(/\/$/, '');
141
+ await fetch(`${base}${path}`, {
142
+ method: 'POST',
143
+ headers: {
144
+ 'Accept': 'application/json',
145
+ 'Content-Type': 'application/json',
146
+ 'X-Partner-Api-Key': partnerKey,
147
+ },
148
+ body: JSON.stringify(payload),
149
+ keepalive: true,
150
+ credentials: 'omit',
151
+ });
152
+ } catch (e) {
153
+ console.warn('[LinkItBe] failed to send ' + eventType, e);
154
+ }
155
+ }
156
+
157
+ async function emitViewRendered(articleId, integrations, appliedCount) {
158
+ if (appliedCount <= 0) return;
159
+ const integ = firstIntegrationWithProcessingId(integrations);
160
+ if (!integ) return;
161
+ await emitSdkEvent('view_rendered', '/v1/events/view-rendered', articleId, integ, { integrations_count: appliedCount });
162
+ }
163
+
164
+ function attachIntegrationViewableObserver(articleId, contentBlock) {
165
+ if (!articleId) return;
166
+ const io = new IntersectionObserver((entries) => {
167
+ for (let i = 0; i < entries.length; i++) {
168
+ const en = entries[i];
169
+ if (!en.isIntersecting || en.intersectionRatio < 0.5) continue;
170
+ const linkEl = en.target;
171
+ if (linkEl.dataset.lbViewableSent === '1') continue;
172
+ let integration;
173
+ try {
174
+ integration = JSON.parse(decodeURIComponent(linkEl.dataset.lbIntegration || ''));
175
+ } catch (e) {
176
+ io.unobserve(linkEl);
177
+ continue;
178
+ }
179
+ linkEl.dataset.lbViewableSent = '1';
180
+ emitSdkEvent('integration_viewable', '/v1/events/integration-viewable', articleId, integration);
181
+ io.unobserve(linkEl);
182
+ }
183
+ }, { threshold: [0.5] });
184
+ contentBlock.querySelectorAll('.__lb_link').forEach((linkEl) => io.observe(linkEl));
185
+ }
186
+
187
+ function filterIntegrationsByAllowedFormats(integrations) {
188
+ const allowed = config.allowedFormats;
189
+ if (allowed == null || !Array.isArray(allowed)) return integrations;
190
+ return integrations.filter(i => allowed.includes(i.type));
191
+ }
192
+
193
+
194
+ // ============================================================
195
+ // 4. ПРЕВЬЮ
196
+ // ============================================================
197
+
198
+ function createPreview() {
199
+ const preview = document.createElement('div');
200
+ preview.id = '__lb_preview';
201
+ preview.style.cssText = `
202
+ position: fixed;
203
+ display: none;
204
+ background: white;
205
+ border-radius: 12px;
206
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
207
+ padding: 12px;
208
+ z-index: 99999;
209
+ width: 180px;
210
+ font-family: Manrope, sans-serif;
211
+ pointer-events: none;
212
+ `;
213
+ document.body.appendChild(preview);
214
+ return preview;
215
+ }
216
+
217
+ function showPreview(preview, integration, x, y, styles_config) {
218
+ if (!integration.preview || !integration.preview.img) {
219
+ preview.style.display = 'none';
220
+ return;
221
+ }
222
+
223
+ preview.replaceChildren();
224
+
225
+ const image = document.createElement('img');
226
+ image.src = String(integration.preview.img);
227
+ image.style.cssText = 'width:100%;border-radius:8px;display:block;';
228
+ preview.appendChild(image);
229
+
230
+ const title = document.createElement('div');
231
+ title.style.cssText = 'margin-top:8px;font-size:13px;color:#343b4c;font-weight:600;';
232
+ title.textContent = integration.preview.title ? String(integration.preview.title) : '';
233
+ preview.appendChild(title);
234
+
235
+ const price = document.createElement('div');
236
+ price.style.cssText = 'font-size:12px;color:#777;margin-top:2px;';
237
+ price.textContent = integration.preview.price ? String(integration.preview.price) : '';
238
+ preview.appendChild(price);
239
+
240
+ const cta = document.createElement('div');
241
+ cta.style.cssText = `font-size:12px;color:${styles_config.link_color};margin-top:2px;`;
242
+ cta.textContent = integration.preview.cta ? String(integration.preview.cta) : '';
243
+ preview.appendChild(cta);
244
+
245
+ preview.style.left = (x + 16) + 'px';
246
+ preview.style.top = (y - 60) + 'px';
247
+ preview.style.display = 'block';
248
+ }
249
+
250
+
251
+ // ============================================================
252
+ // 5. KEYWORD: plain, матчи, Range
253
+ // ============================================================
254
+
255
+ function escapeRegex(s) {
256
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
257
+ }
258
+
259
+ function isInsideLbLink(node) {
260
+ const el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
261
+ return !!(el && el.closest && el.closest('a.__lb_link'));
262
+ }
263
+
264
+ function getTextNodesInBlock(blockEl) {
265
+ const out = [];
266
+ const walker = document.createTreeWalker(
267
+ blockEl,
268
+ NodeFilter.SHOW_TEXT,
269
+ {
270
+ acceptNode(node) {
271
+ if (isInsideLbLink(node)) return NodeFilter.FILTER_REJECT;
272
+ return NodeFilter.FILTER_ACCEPT;
273
+ }
274
+ }
275
+ );
276
+ let n;
277
+ while ((n = walker.nextNode())) out.push(n);
278
+ return out;
279
+ }
280
+
281
+ function buildPlainAndSpans(contentBlock) {
282
+ const joiner = config.plainBlockJoiner != null ? config.plainBlockJoiner : '\n';
283
+ const sel = config.plainBlockSelector || 'p, blockquote, h1, h2, h3, h4, h5, h6';
284
+ const blocks = contentBlock.querySelectorAll(sel);
285
+ const spans = [];
286
+ let plain = '';
287
+
288
+ blocks.forEach((block, i) => {
289
+ if (i > 0) plain += joiner;
290
+ const textNodes = getTextNodesInBlock(block);
291
+ for (const node of textNodes) {
292
+ const t = node.textContent;
293
+ if (!t.length) continue;
294
+ const g0 = plain.length;
295
+ plain += t;
296
+ spans.push({ g0, g1: plain.length, node });
297
+ }
298
+ });
299
+
300
+ return { plain, spans };
301
+ }
302
+
303
+ function findKeywordMatches(plain, keyword) {
304
+ if (!keyword) return [];
305
+ const re = new RegExp(escapeRegex(keyword), 'gi');
306
+ const matches = [];
307
+ let m;
308
+ while ((m = re.exec(plain)) !== null) {
309
+ matches.push({ start: m.index, end: m.index + m[0].length });
310
+ if (m[0].length === 0 && re.lastIndex === m.index) re.lastIndex++;
311
+ }
312
+ return matches;
313
+ }
314
+
315
+ function pickMatchSpan(matches, position) {
316
+ if (!matches.length) return null;
317
+ if (matches.length === 1) return matches[0];
318
+
319
+ const pos = position || {};
320
+ const mi = pos.match_index;
321
+ if (mi != null && Number.isFinite(Number(mi))) {
322
+ const idx = Number(mi);
323
+ if (idx >= 0 && idx < matches.length) return matches[idx];
324
+ console.warn('[LinkItBe] match_index out of range:', mi, 'n=', matches.length);
325
+ }
326
+
327
+ const os = pos.offset_start;
328
+ const oe = pos.offset_end;
329
+ if (os != null && Number.isFinite(Number(os))) {
330
+ const nos = Number(os);
331
+ const noe = oe != null && Number.isFinite(Number(oe)) ? Number(oe) : null;
332
+ let best = matches[0];
333
+ let bestScore = Infinity;
334
+ for (const m of matches) {
335
+ const d1 = Math.abs(m.start - nos);
336
+ const d2 = noe != null ? Math.abs(m.end - noe) : 0;
337
+ const score = d1 * 1e9 + d2;
338
+ if (score < bestScore) {
339
+ bestScore = score;
340
+ best = m;
341
+ }
342
+ }
343
+ return best;
344
+ }
345
+
346
+ console.warn('[LinkItBe] ambiguous matches, skipping (no match_index / offset_start):', matches.length);
347
+ return null;
348
+ }
349
+
350
+ function pointForInclusiveStart(gs, spans, plainLen) {
351
+ if (gs < 0 || gs >= plainLen || !spans.length) return null;
352
+ const s = spans.find(seg => gs >= seg.g0 && gs < seg.g1);
353
+ if (!s) return null;
354
+ return { node: s.node, offset: gs - s.g0 };
355
+ }
356
+
357
+ function pointForExclusiveEnd(ge, spans, plainLen) {
358
+ if (!spans.length) return null;
359
+ if (ge <= 0) return { node: spans[0].node, offset: 0 };
360
+ if (ge >= plainLen) {
361
+ const last = spans[spans.length - 1];
362
+ return { node: last.node, offset: last.node.textContent.length };
363
+ }
364
+ for (let i = 0; i < spans.length; i++) {
365
+ const s = spans[i];
366
+ if (ge > s.g0 && ge <= s.g1) {
367
+ return { node: s.node, offset: ge - s.g0 };
368
+ }
369
+ if (ge === s.g0) {
370
+ return { node: s.node, offset: 0 };
371
+ }
372
+ }
373
+ for (let i = 0; i < spans.length; i++) {
374
+ if (ge < spans[i].g0) {
375
+ return { node: spans[i].node, offset: 0 };
376
+ }
377
+ }
378
+ return null;
379
+ }
380
+
381
+ function wrapRangeWithAnchor(range, anchor) {
382
+ try {
383
+ range.surroundContents(anchor);
384
+ } catch (e) {
385
+ const contents = range.extractContents();
386
+ while (contents.firstChild) anchor.appendChild(contents.firstChild);
387
+ range.insertNode(anchor);
388
+ }
389
+ }
390
+
391
+ function sanitizeIntegrationLink(rawLink) {
392
+ if (rawLink == null) {
393
+ return null;
394
+ }
395
+
396
+ try {
397
+ const url = new URL(String(rawLink), window.location.origin);
398
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
399
+ return null;
400
+ }
401
+ return url.toString();
402
+ } catch (e) {
403
+ return null;
404
+ }
405
+ }
406
+
407
+ function createLinkElement(integration, linkText, linkStyle) {
408
+ const sanitizedLink = sanitizeIntegrationLink(integration.link);
409
+ if (!sanitizedLink) {
410
+ return null;
411
+ }
412
+
413
+ const a = document.createElement('a');
414
+ a.href = sanitizedLink;
415
+ a.target = '_blank';
416
+ a.rel = 'noopener noreferrer sponsored nofollow';
417
+ a.className = '__lb_link';
418
+ a.setAttribute('data-lb-integration', encodeURIComponent(JSON.stringify(integration)));
419
+ a.style.cssText = linkStyle.replace(/\s+/g, ' ').trim();
420
+ a.textContent = linkText;
421
+ return a;
422
+ }
423
+
424
+ function resolveKeywordSpan(contentBlock, integration) {
425
+ const { plain, spans } = buildPlainAndSpans(contentBlock);
426
+ const matches = findKeywordMatches(plain, integration.keyword);
427
+ const span = pickMatchSpan(matches, integration.position);
428
+ if (!span) return null;
429
+ const linkText = plain.slice(span.start, span.end);
430
+ return { plain, spans, span, linkText };
431
+ }
432
+
433
+ function applyKeywordWrap(contentBlock, integration, linkStyle) {
434
+ const resolved = resolveKeywordSpan(contentBlock, integration);
435
+ if (!resolved) {
436
+ console.warn('[LinkItBe] no span for keyword:', integration.keyword);
437
+ return false;
438
+ }
439
+
440
+ const { plain, spans, span, linkText } = resolved;
441
+ const startPt = pointForInclusiveStart(span.start, spans, plain.length);
442
+ const endPt = pointForExclusiveEnd(span.end, spans, plain.length);
443
+ if (!startPt || !endPt) {
444
+ console.warn('[LinkItBe] could not map span to DOM');
445
+ return false;
446
+ }
447
+
448
+ const slice = plain.slice(span.start, span.end);
449
+ if (slice.toLowerCase() !== linkText.toLowerCase()) {
450
+ console.warn('[LinkItBe] plain slice mismatch');
451
+ return false;
452
+ }
453
+
454
+ const range = document.createRange();
455
+ range.setStart(startPt.node, startPt.offset);
456
+ range.setEnd(endPt.node, endPt.offset);
457
+
458
+ const anchor = createLinkElement(integration, slice, linkStyle);
459
+ if (!anchor) {
460
+ console.warn('[LinkItBe] invalid integration link:', integration.link);
461
+ return false;
462
+ }
463
+ wrapRangeWithAnchor(range, anchor);
464
+ return true;
465
+ }
466
+
467
+ const MINI_OFFER_PLACEHOLDER = '{anchor_text}';
468
+
469
+ function applyMiniOffer(contentBlock, integration, linkStyle) {
470
+ const sel = config.plainBlockSelector || 'p, blockquote, h1, h2, h3, h4, h5, h6';
471
+ const blocks = contentBlock.querySelectorAll(sel);
472
+ if (!blocks.length) {
473
+ console.warn('[LinkItBe] mini-offer: no content blocks');
474
+ return false;
475
+ }
476
+
477
+ const lastBlock = blocks[blocks.length - 1];
478
+ const outro = document.createElement('p');
479
+ outro.className = '__lb_mini_offer';
480
+ outro.style.cssText = 'margin-top:16px;font-size:15px;font-family:Manrope, sans-serif;color:#343b4c;';
481
+
482
+ const adLabel = document.createElement('span');
483
+ adLabel.style.cssText = 'font-size:11px;color:#999;display:block;margin-bottom:4px;';
484
+ adLabel.textContent = '#реклама';
485
+ outro.appendChild(adLabel);
486
+
487
+ const body = document.createElement('span');
488
+ const template = integration.text != null ? String(integration.text) : '';
489
+ const anchorLabel = integration.anchor_text != null ? String(integration.anchor_text) : '';
490
+ const ph = template.indexOf(MINI_OFFER_PLACEHOLDER);
491
+ const before = ph >= 0 ? template.slice(0, ph) : template;
492
+ const after = ph >= 0 ? template.slice(ph + MINI_OFFER_PLACEHOLDER.length) : '';
493
+
494
+ if (before) body.appendChild(document.createTextNode(before));
495
+ const anchor = createLinkElement(integration, anchorLabel, linkStyle);
496
+ if (!anchor) {
497
+ console.warn('[LinkItBe] mini-offer invalid integration link:', integration.link);
498
+ return false;
499
+ }
500
+ body.appendChild(anchor);
501
+ if (after) body.appendChild(document.createTextNode(after));
502
+
503
+ outro.appendChild(body);
504
+ lastBlock.after(outro);
505
+ return true;
506
+ }
507
+
508
+ function applyIntegrations(articleId, integrations, contentBlock, style_config) {
509
+ if (!contentBlock) {
510
+ console.warn('[LinkItBe] Content block not provided');
511
+ const active = filterIntegrationsByAllowedFormats(integrations);
512
+ return { appliedCount: 0, integrations: active };
513
+ }
514
+
515
+ const preview = config.showPreview ? createPreview() : null;
516
+
517
+ const linkStyle = `
518
+ color:${style_config.link_color};
519
+ font-weight:${style_config.link_weight};
520
+ text-decoration:${style_config.link_decoration};
521
+ cursor:pointer;
522
+ `;
523
+
524
+ const active = filterIntegrationsByAllowedFormats(integrations);
525
+ let appliedCount = 0;
526
+ let pending = active.filter(i => i.type === 'keyword');
527
+
528
+ while (pending.length) {
529
+ const rows = [];
530
+ for (const integ of pending) {
531
+ const r = resolveKeywordSpan(contentBlock, integ);
532
+ if (r) rows.push({ integ, spanStart: r.span.start });
533
+ }
534
+ if (!rows.length) {
535
+ pending.forEach(i => console.warn('[LinkItBe] unresolved keyword:', i.keyword));
536
+ break;
537
+ }
538
+
539
+ rows.sort((a, b) => b.spanStart - a.spanStart);
540
+ const { integ } = rows[0];
541
+ if (applyKeywordWrap(contentBlock, integ, linkStyle)) {
542
+ appliedCount += 1;
543
+ }
544
+ pending = pending.filter(i => i !== integ);
545
+ }
546
+
547
+ active.filter(i => i.type === 'mini-offer').forEach(integ => {
548
+ if (applyMiniOffer(contentBlock, integ, linkStyle)) {
549
+ appliedCount += 1;
550
+ } else {
551
+ console.warn('[LinkItBe] mini-offer failed');
552
+ }
553
+ });
554
+
555
+ if (preview) {
556
+ contentBlock.querySelectorAll('.__lb_link').forEach((linkEl) => {
557
+ let integration;
558
+ try {
559
+ integration = JSON.parse(decodeURIComponent(linkEl.dataset.lbIntegration || ''));
560
+ } catch (e) {
561
+ console.warn('[LinkItBe] invalid integration payload for preview', e);
562
+ return;
563
+ }
564
+ if (!integration.preview || !integration.preview.img) {
565
+ return;
566
+ }
567
+ linkEl.addEventListener('mouseenter', (e) => {
568
+ showPreview(preview, integration, e.clientX, e.clientY, style_config);
569
+ if (linkEl.dataset.lbPreviewSent === '1' || !articleId) return;
570
+ linkEl.dataset.lbPreviewSent = '1';
571
+ emitSdkEvent('preview_viewed', '/v1/events/preview-viewed', articleId, integration);
572
+ });
573
+ linkEl.addEventListener('mousemove', (e) => {
574
+ preview.style.left = (e.clientX + 16) + 'px';
575
+ preview.style.top = (e.clientY - 60) + 'px';
576
+ });
577
+ linkEl.addEventListener('mouseleave', () => { preview.style.display = 'none'; });
578
+ });
579
+ }
580
+
581
+ attachIntegrationViewableObserver(articleId, contentBlock);
582
+
583
+ return { appliedCount, integrations: active };
584
+ }
585
+
586
+
587
+ // ============================================================
588
+ // INIT AND MARKUP ARTICLE
589
+ // ============================================================
590
+
591
+ async function markupArticle(articleId, contentBlock, data) {
592
+ if (contentBlock.dataset.lbProcessed === '1') return;
593
+ contentBlock.dataset.lbProcessed = '1';
594
+
595
+ const integrations = Array.isArray(data.integrations) ? data.integrations : [];
596
+
597
+ const style_config = data.config.style;
598
+ const result = applyIntegrations(articleId, integrations, contentBlock, style_config);
599
+
600
+ await emitViewRendered(articleId, result.integrations, result.appliedCount);
601
+ }
602
+
603
+ function ensureParams(articleId, contentBlock) {
604
+ if (!articleId) {
605
+ console.warn('[LinkItBe] Article ID not found');
606
+ return false;
607
+ }
608
+ if (!contentBlock) {
609
+ console.warn('[LinkItBe] Content block not found for article:', articleId);
610
+ return false;
611
+ }
612
+ return true;
613
+ }
614
+
615
+ async function initArticle(articleId, contentBlock) {
616
+ if (!ensureParams(articleId, contentBlock)) return;
617
+ if (contentBlock.dataset.lbProcessed === '1') return;
618
+
619
+ const data = await fetchMarkup(articleId);
620
+ if (!data) return;
621
+ await markupArticle(articleId, contentBlock, data);
622
+ }
623
+
624
+
625
+ // ============================================================
626
+ // FEED AUTO-MARKUP: MutationObserver на новые узлы в ленте
627
+ // ============================================================
628
+
629
+ function processFeedAddedNode(node) {
630
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
631
+ const contentBlock = node.querySelector(config.contentSelector);
632
+ if (!contentBlock) return;
633
+ const feedArticleId = resolveArticleId(contentBlock);
634
+ initArticle(feedArticleId, contentBlock);
635
+ }
636
+
637
+ function startFeedAutoMarkup(feedRoot) {
638
+ if (!feedRoot) {
639
+ console.warn('[LinkItBe] feedRoot not provided for auto markup');
640
+ return;
641
+ }
642
+
643
+ const observer = new MutationObserver((mutations) => {
644
+ for (const m of mutations) {
645
+ for (let i = 0; i < m.addedNodes.length; i++) {
646
+ processFeedAddedNode(m.addedNodes[i]);
647
+ }
648
+ }
649
+ });
650
+ observer.observe(feedRoot, { childList: true, subtree: true });
651
+ }
652
+
653
+
654
+ // ============================================================
655
+ // BOOT
656
+ // ============================================================
657
+
658
+ async function boot() {
659
+ const contentBlock = document.querySelector(config.contentSelector);
660
+ const articleId = resolveArticleId(contentBlock);
661
+ await initArticle(articleId, contentBlock);
662
+
663
+ if(config.enableFeedAutoProcess !== true) return;
664
+ const feedRoot = document.querySelector(config.feedObserverRootSelector);
665
+ if (!feedRoot) {
666
+ console.warn('[LinkItBe] feedObserverRootSelector not found:', config.feedObserverRootSelector);
667
+ return;
668
+ }
669
+ startFeedAutoMarkup(feedRoot);
670
+ }
671
+
672
+ if (document.readyState === 'loading') {
673
+ document.addEventListener('DOMContentLoaded', boot);
674
+ } else {
675
+ if (config.bootOnLoad === true) {
676
+ boot();
677
+ }
678
+ }
679
+
680
+
681
+ // ============================================================
682
+ // SDK API
683
+ // ============================================================
684
+
685
+ window.LinkItBe = Object.assign({}, window.LinkItBe || {}, {
686
+ initArticle,
687
+ startFeedAutoMarkup
688
+ });
689
+ })();
@@ -1,627 +1 @@
1
- (function() {
2
-
3
- // ============================================================
4
- // КОНФИГ (переопределяется через window.LinkItBeConfig)
5
- // ============================================================
6
-
7
- const config = Object.assign({
8
- domain: window.location.hostname,
9
- contentSelector: null,
10
- useRemoteContentSelector: true,
11
- showPreview: true,
12
- useRemoteStyles: false,
13
- plainBlockJoiner: '\n',
14
- plainBlockSelector: 'p, blockquote, h1, h2, h3, h4, h5, h6',
15
- // null / не массив — все типы интеграций; иначе только перечисленные (например ['keyword', 'mini-offer']).
16
- allowedFormats: ['keyword', 'mini-offer'],
17
- apiBaseUrl: 'https://api.linkitbe.com',
18
- partnerApiKey: null,
19
- sdkVersion: '1.1.0',
20
- styles: {
21
- linkColor: '#e2187b',
22
- linkWeight: '700',
23
- linkDecoration: 'underline',
24
- }
25
- }, window.LinkItBeConfig || {});
26
-
27
-
28
- // ============================================================
29
- // 1. ПОЛУЧЕНИЕ ID СТАТЬИ
30
- // ============================================================
31
-
32
- function getScriptQueryParam(name) {
33
- const scripts = document.getElementsByTagName('script');
34
- for (let i = scripts.length - 1; i >= 0; i--) {
35
- const src = scripts[i].getAttribute('src') || '';
36
- if (!src || src.indexOf('linkitbe_sdk.js') === -1) continue;
37
- try {
38
- const u = new URL(src, window.location.origin);
39
- const v = u.searchParams.get(name);
40
- if (v && v.trim()) return v.trim();
41
- } catch (e) {
42
- // ignore parse issues and continue fallback chain
43
- }
44
- }
45
- return null;
46
- }
47
-
48
- function getArticleId() {
49
- const fromConfig = config.linkitbe_article_id ? String(config.linkitbe_article_id).trim() : "";
50
- if (fromConfig) return fromConfig;
51
-
52
- const fromScriptParam = getScriptQueryParam('linkitbe_article_id');
53
- if (fromScriptParam) return fromScriptParam;
54
-
55
- // Вариант 1: из URL
56
- const urlMatch = window.location.pathname.match(/\/(\d{6,})-/);
57
- if (urlMatch) return urlMatch[1];
58
-
59
- // Вариант 2: из id параграфов (id="56287698_1")
60
- const firstP = document.querySelector('p[id]');
61
- if (firstP) {
62
- const idMatch = firstP.id.match(/^(\d+)_/);
63
- if (idMatch) return idMatch[1];
64
- }
65
-
66
- // Вариант 3: из data-атрибута gigachat контейнера
67
- const gigachat = document.querySelector('[id^="rambler-gigachat-chat-container:"]');
68
- if (gigachat) {
69
- const containerMatch = gigachat.id.match(/(\d{6,})/);
70
- if (containerMatch) return containerMatch[1];
71
- }
72
-
73
- return null;
74
- }
75
-
76
-
77
- // ============================================================
78
- // 2. ЗАПРОС РАЗМЕТКИ
79
- // ============================================================
80
-
81
- async function fetchMarkup(articleId) {
82
- const base = String(config.apiBaseUrl || 'https://api.linkitbe.com').replace(/\/$/, '');
83
- const url = `${base}/v1/markup?article_id=${encodeURIComponent(articleId)}&domain=${encodeURIComponent(config.domain)}`;
84
-
85
- const headers = { 'Accept': 'application/json' };
86
- const partnerKey = config.partnerApiKey ? String(config.partnerApiKey).trim() : '';
87
- if (partnerKey) {
88
- headers['X-Partner-Api-Key'] = partnerKey;
89
- }
90
-
91
- const res = await fetch(url, {
92
- method: 'GET',
93
- headers,
94
- credentials: 'omit',
95
- });
96
-
97
- if (!res.ok) {
98
- throw new Error(`[LinkItBe] markup request failed: ${res.status}`);
99
- }
100
-
101
- return await res.json();
102
- }
103
-
104
- function getOrCreateSessionId() {
105
- const key = '__lb_session_id';
106
- try {
107
- const existing = window.localStorage.getItem(key);
108
- if (existing && existing.trim()) return existing.trim();
109
- const generated = (window.crypto && window.crypto.randomUUID)
110
- ? window.crypto.randomUUID()
111
- : `${Date.now()}-${Math.random().toString(16).slice(2)}`;
112
- window.localStorage.setItem(key, generated);
113
- return generated;
114
- } catch (e) {
115
- return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
116
- }
117
- }
118
-
119
- function extractProcessingIdFromIntegrations(integrations) {
120
- if (!Array.isArray(integrations)) return null;
121
- for (const integration of integrations) {
122
- const link = integration && integration.link ? String(integration.link) : '';
123
- if (!link) continue;
124
- try {
125
- const u = new URL(link, window.location.origin);
126
- const raw = u.searchParams.get('processing_id');
127
- const value = Number(raw);
128
- if (Number.isFinite(value) && value > 0) return value;
129
- } catch (e) {
130
- // ignore malformed link and continue
131
- }
132
- }
133
- return null;
134
- }
135
-
136
- async function emitViewRendered(articleId, integrations, appliedCount) {
137
- const processingId = extractProcessingIdFromIntegrations(integrations);
138
- if (!processingId || appliedCount <= 0) return;
139
-
140
- const partnerKey = config.partnerApiKey ? String(config.partnerApiKey).trim() : '';
141
- if (!partnerKey) return;
142
-
143
- const payload = {
144
- event_id: (window.crypto && window.crypto.randomUUID)
145
- ? window.crypto.randomUUID()
146
- : `${Date.now()}-${Math.random().toString(16).slice(2)}`,
147
- event_type: 'view_rendered',
148
- event_time: new Date().toISOString(),
149
- article_id: articleId,
150
- domain: config.domain,
151
- processing_id: processingId,
152
- placement_link: window.location.href,
153
- integrations_count: appliedCount,
154
- sdk_version: String(config.sdkVersion || ''),
155
- session_id: getOrCreateSessionId(),
156
- referrer: document.referrer || null,
157
- };
158
-
159
- try {
160
- const base = String(config.apiBaseUrl || 'https://api.linkitbe.com').replace(/\/$/, '');
161
- await fetch(`${base}/v1/events/view-rendered`, {
162
- method: 'POST',
163
- headers: {
164
- 'Accept': 'application/json',
165
- 'Content-Type': 'application/json',
166
- 'X-Partner-Api-Key': partnerKey,
167
- },
168
- body: JSON.stringify(payload),
169
- keepalive: true,
170
- credentials: 'omit',
171
- });
172
- } catch (e) {
173
- console.warn('[LinkItBe] failed to send view_rendered event', e);
174
- }
175
- }
176
-
177
-
178
-
179
- // ============================================================
180
- // 3. ПРИМЕНЕНИЕ КОНФИГА ИЗ API
181
- // ============================================================
182
-
183
- function resolveConfig(remoteConfig) {
184
- if (remoteConfig) {
185
- if (config.useRemoteContentSelector && remoteConfig.content_selector) {
186
- config.contentSelector = remoteConfig.content_selector;
187
- }
188
- if (config.useRemoteStyles && remoteConfig.style) {
189
- config.styles = {
190
- linkColor: remoteConfig.style.link_color || config.styles.linkColor,
191
- linkWeight: remoteConfig.style.link_weight || config.styles.linkWeight,
192
- linkDecoration: remoteConfig.style.link_decoration || config.styles.linkDecoration,
193
- };
194
- }
195
- if (Array.isArray(remoteConfig.allowed_formats)) {
196
- config.allowedFormats = remoteConfig.allowed_formats;
197
- }
198
- }
199
- }
200
-
201
- function filterIntegrationsByAllowedFormats(integrations) {
202
- const allowed = config.allowedFormats;
203
- if (allowed == null || !Array.isArray(allowed)) return integrations;
204
- return integrations.filter(i => allowed.includes(i.type));
205
- }
206
-
207
-
208
- // ============================================================
209
- // 4. ПРЕВЬЮ
210
- // ============================================================
211
-
212
- function createPreview() {
213
- const preview = document.createElement('div');
214
- preview.id = '__lb_preview';
215
- preview.style.cssText = `
216
- position: fixed;
217
- display: none;
218
- background: white;
219
- border-radius: 12px;
220
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
221
- padding: 12px;
222
- z-index: 99999;
223
- width: 180px;
224
- font-family: Manrope, sans-serif;
225
- pointer-events: none;
226
- `;
227
- document.body.appendChild(preview);
228
- return preview;
229
- }
230
-
231
- function showPreview(preview, integration, x, y) {
232
- if (!integration.preview || !integration.preview.img) {
233
- preview.style.display = 'none';
234
- return;
235
- }
236
-
237
- preview.replaceChildren();
238
-
239
- const image = document.createElement('img');
240
- image.src = String(integration.preview.img);
241
- image.style.cssText = 'width:100%;border-radius:8px;display:block;';
242
- preview.appendChild(image);
243
-
244
- const title = document.createElement('div');
245
- title.style.cssText = 'margin-top:8px;font-size:13px;color:#343b4c;font-weight:600;';
246
- title.textContent = integration.preview.title ? String(integration.preview.title) : '';
247
- preview.appendChild(title);
248
-
249
- const price = document.createElement('div');
250
- price.style.cssText = 'font-size:12px;color:#777;margin-top:2px;';
251
- price.textContent = integration.preview.price ? String(integration.preview.price) : '';
252
- preview.appendChild(price);
253
-
254
- const cta = document.createElement('div');
255
- cta.style.cssText = `font-size:12px;color:${config.styles.linkColor};margin-top:2px;`;
256
- cta.textContent = integration.preview.cta ? String(integration.preview.cta) : '';
257
- preview.appendChild(cta);
258
-
259
- preview.style.left = (x + 16) + 'px';
260
- preview.style.top = (y - 60) + 'px';
261
- preview.style.display = 'block';
262
- }
263
-
264
-
265
- // ============================================================
266
- // 5. KEYWORD: plain, матчи, Range
267
- // ============================================================
268
-
269
- function escapeRegex(s) {
270
- return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
271
- }
272
-
273
- function isInsideLbLink(node) {
274
- const el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
275
- return !!(el && el.closest && el.closest('a.__lb_link'));
276
- }
277
-
278
- function getTextNodesInBlock(blockEl) {
279
- const out = [];
280
- const walker = document.createTreeWalker(
281
- blockEl,
282
- NodeFilter.SHOW_TEXT,
283
- {
284
- acceptNode(node) {
285
- if (isInsideLbLink(node)) return NodeFilter.FILTER_REJECT;
286
- return NodeFilter.FILTER_ACCEPT;
287
- }
288
- }
289
- );
290
- let n;
291
- while ((n = walker.nextNode())) out.push(n);
292
- return out;
293
- }
294
-
295
- function buildPlainAndSpans(contentBlock) {
296
- const joiner = config.plainBlockJoiner != null ? config.plainBlockJoiner : '\n';
297
- const sel = config.plainBlockSelector || 'p, blockquote, h1, h2, h3, h4, h5, h6';
298
- const blocks = contentBlock.querySelectorAll(sel);
299
- const spans = [];
300
- let plain = '';
301
-
302
- blocks.forEach((block, i) => {
303
- if (i > 0) plain += joiner;
304
- const textNodes = getTextNodesInBlock(block);
305
- for (const node of textNodes) {
306
- const t = node.textContent;
307
- if (!t.length) continue;
308
- const g0 = plain.length;
309
- plain += t;
310
- spans.push({ g0, g1: plain.length, node });
311
- }
312
- });
313
-
314
- return { plain, spans };
315
- }
316
-
317
- function findKeywordMatches(plain, keyword) {
318
- if (!keyword) return [];
319
- const re = new RegExp(escapeRegex(keyword), 'gi');
320
- const matches = [];
321
- let m;
322
- while ((m = re.exec(plain)) !== null) {
323
- matches.push({ start: m.index, end: m.index + m[0].length });
324
- if (m[0].length === 0 && re.lastIndex === m.index) re.lastIndex++;
325
- }
326
- return matches;
327
- }
328
-
329
- function pickMatchSpan(matches, position) {
330
- if (!matches.length) return null;
331
- if (matches.length === 1) return matches[0];
332
-
333
- const pos = position || {};
334
- const mi = pos.match_index;
335
- if (mi != null && Number.isFinite(Number(mi))) {
336
- const idx = Number(mi);
337
- if (idx >= 0 && idx < matches.length) return matches[idx];
338
- console.warn('[LinkItBe] match_index out of range:', mi, 'n=', matches.length);
339
- }
340
-
341
- const os = pos.offset_start;
342
- const oe = pos.offset_end;
343
- if (os != null && Number.isFinite(Number(os))) {
344
- const nos = Number(os);
345
- const noe = oe != null && Number.isFinite(Number(oe)) ? Number(oe) : null;
346
- let best = matches[0];
347
- let bestScore = Infinity;
348
- for (const m of matches) {
349
- const d1 = Math.abs(m.start - nos);
350
- const d2 = noe != null ? Math.abs(m.end - noe) : 0;
351
- const score = d1 * 1e9 + d2;
352
- if (score < bestScore) {
353
- bestScore = score;
354
- best = m;
355
- }
356
- }
357
- return best;
358
- }
359
-
360
- console.warn('[LinkItBe] ambiguous matches, skipping (no match_index / offset_start):', matches.length);
361
- return null;
362
- }
363
-
364
- function pointForInclusiveStart(gs, spans, plainLen) {
365
- if (gs < 0 || gs >= plainLen || !spans.length) return null;
366
- const s = spans.find(seg => gs >= seg.g0 && gs < seg.g1);
367
- if (!s) return null;
368
- return { node: s.node, offset: gs - s.g0 };
369
- }
370
-
371
- function pointForExclusiveEnd(ge, spans, plainLen) {
372
- if (!spans.length) return null;
373
- if (ge <= 0) return { node: spans[0].node, offset: 0 };
374
- if (ge >= plainLen) {
375
- const last = spans[spans.length - 1];
376
- return { node: last.node, offset: last.node.textContent.length };
377
- }
378
- for (let i = 0; i < spans.length; i++) {
379
- const s = spans[i];
380
- if (ge > s.g0 && ge <= s.g1) {
381
- return { node: s.node, offset: ge - s.g0 };
382
- }
383
- if (ge === s.g0) {
384
- return { node: s.node, offset: 0 };
385
- }
386
- }
387
- for (let i = 0; i < spans.length; i++) {
388
- if (ge < spans[i].g0) {
389
- return { node: spans[i].node, offset: 0 };
390
- }
391
- }
392
- return null;
393
- }
394
-
395
- function wrapRangeWithAnchor(range, anchor) {
396
- try {
397
- range.surroundContents(anchor);
398
- } catch (e) {
399
- const contents = range.extractContents();
400
- while (contents.firstChild) anchor.appendChild(contents.firstChild);
401
- range.insertNode(anchor);
402
- }
403
- }
404
-
405
- function sanitizeIntegrationLink(rawLink) {
406
- if (rawLink == null) {
407
- return null;
408
- }
409
-
410
- try {
411
- const url = new URL(String(rawLink), window.location.origin);
412
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
413
- return null;
414
- }
415
- return url.toString();
416
- } catch (e) {
417
- return null;
418
- }
419
- }
420
-
421
- function createLinkElement(integration, linkText, linkStyle) {
422
- const sanitizedLink = sanitizeIntegrationLink(integration.link);
423
- if (!sanitizedLink) {
424
- return null;
425
- }
426
-
427
- const a = document.createElement('a');
428
- a.href = sanitizedLink;
429
- a.target = '_blank';
430
- a.rel = 'noopener noreferrer sponsored nofollow';
431
- a.className = '__lb_link';
432
- a.setAttribute('data-lb-integration', encodeURIComponent(JSON.stringify(integration)));
433
- a.style.cssText = linkStyle.replace(/\s+/g, ' ').trim();
434
- a.textContent = linkText;
435
- return a;
436
- }
437
-
438
- function resolveKeywordSpan(contentBlock, integration) {
439
- const { plain, spans } = buildPlainAndSpans(contentBlock);
440
- const matches = findKeywordMatches(plain, integration.keyword);
441
- const span = pickMatchSpan(matches, integration.position);
442
- if (!span) return null;
443
- const linkText = plain.slice(span.start, span.end);
444
- return { plain, spans, span, linkText };
445
- }
446
-
447
- function applyKeywordWrap(contentBlock, integration, linkStyle) {
448
- const resolved = resolveKeywordSpan(contentBlock, integration);
449
- if (!resolved) {
450
- console.warn('[LinkItBe] no span for keyword:', integration.keyword);
451
- return false;
452
- }
453
-
454
- const { plain, spans, span, linkText } = resolved;
455
- const startPt = pointForInclusiveStart(span.start, spans, plain.length);
456
- const endPt = pointForExclusiveEnd(span.end, spans, plain.length);
457
- if (!startPt || !endPt) {
458
- console.warn('[LinkItBe] could not map span to DOM');
459
- return false;
460
- }
461
-
462
- const slice = plain.slice(span.start, span.end);
463
- if (slice.toLowerCase() !== linkText.toLowerCase()) {
464
- console.warn('[LinkItBe] plain slice mismatch');
465
- return false;
466
- }
467
-
468
- const range = document.createRange();
469
- range.setStart(startPt.node, startPt.offset);
470
- range.setEnd(endPt.node, endPt.offset);
471
-
472
- const anchor = createLinkElement(integration, slice, linkStyle);
473
- if (!anchor) {
474
- console.warn('[LinkItBe] invalid integration link:', integration.link);
475
- return false;
476
- }
477
- wrapRangeWithAnchor(range, anchor);
478
- return true;
479
- }
480
-
481
- const MINI_OFFER_PLACEHOLDER = '{anchor_text}';
482
-
483
- function applyMiniOffer(contentBlock, integration, linkStyle) {
484
- const sel = config.plainBlockSelector || 'p, blockquote, h1, h2, h3, h4, h5, h6';
485
- const blocks = contentBlock.querySelectorAll(sel);
486
- if (!blocks.length) {
487
- console.warn('[LinkItBe] mini-offer: no content blocks');
488
- return false;
489
- }
490
-
491
- const lastBlock = blocks[blocks.length - 1];
492
- const outro = document.createElement('p');
493
- outro.className = '__lb_mini_offer';
494
- outro.style.cssText = 'margin-top:16px;font-size:15px;font-family:Manrope, sans-serif;color:#343b4c;';
495
-
496
- const adLabel = document.createElement('span');
497
- adLabel.style.cssText = 'font-size:11px;color:#999;display:block;margin-bottom:4px;';
498
- adLabel.textContent = '#реклама';
499
- outro.appendChild(adLabel);
500
-
501
- const body = document.createElement('span');
502
- const template = integration.text != null ? String(integration.text) : '';
503
- const anchorLabel = integration.anchor_text != null ? String(integration.anchor_text) : '';
504
- const ph = template.indexOf(MINI_OFFER_PLACEHOLDER);
505
- const before = ph >= 0 ? template.slice(0, ph) : template;
506
- const after = ph >= 0 ? template.slice(ph + MINI_OFFER_PLACEHOLDER.length) : '';
507
-
508
- if (before) body.appendChild(document.createTextNode(before));
509
- const anchor = createLinkElement(integration, anchorLabel, linkStyle);
510
- if (!anchor) {
511
- console.warn('[LinkItBe] mini-offer invalid integration link:', integration.link);
512
- return false;
513
- }
514
- body.appendChild(anchor);
515
- if (after) body.appendChild(document.createTextNode(after));
516
-
517
- outro.appendChild(body);
518
- lastBlock.after(outro);
519
- return true;
520
- }
521
-
522
- function applyIntegrations(integrations) {
523
- const contentBlock = config.contentSelector
524
- ? document.querySelector(config.contentSelector)
525
- : document.body;
526
-
527
- if (!contentBlock) {
528
- console.warn('[LinkItBe] Content block not found:', config.contentSelector);
529
- const active = filterIntegrationsByAllowedFormats(integrations);
530
- return { appliedCount: 0, integrations: active };
531
- }
532
-
533
- const preview = config.showPreview ? createPreview() : null;
534
-
535
- const linkStyle = `
536
- color:${config.styles.linkColor};
537
- font-weight:${config.styles.linkWeight};
538
- text-decoration:${config.styles.linkDecoration};
539
- cursor:pointer;
540
- `;
541
-
542
- const active = filterIntegrationsByAllowedFormats(integrations);
543
- let appliedCount = 0;
544
- let pending = active.filter(i => i.type === 'keyword');
545
-
546
- while (pending.length) {
547
- const rows = [];
548
- for (const integ of pending) {
549
- const r = resolveKeywordSpan(contentBlock, integ);
550
- if (r) rows.push({ integ, spanStart: r.span.start });
551
- }
552
- if (!rows.length) {
553
- pending.forEach(i => console.warn('[LinkItBe] unresolved keyword:', i.keyword));
554
- break;
555
- }
556
-
557
- rows.sort((a, b) => b.spanStart - a.spanStart);
558
- const { integ } = rows[0];
559
- if (applyKeywordWrap(contentBlock, integ, linkStyle)) {
560
- appliedCount += 1;
561
- }
562
- pending = pending.filter(i => i !== integ);
563
- }
564
-
565
- active.filter(i => i.type === 'mini-offer').forEach(integ => {
566
- if (applyMiniOffer(contentBlock, integ, linkStyle)) {
567
- appliedCount += 1;
568
- } else {
569
- console.warn('[LinkItBe] mini-offer failed');
570
- }
571
- });
572
-
573
- if (preview) {
574
- document.querySelectorAll('.__lb_link').forEach(link => {
575
- let integration;
576
- try {
577
- integration = JSON.parse(decodeURIComponent(link.dataset.lbIntegration || ''));
578
- } catch (e) {
579
- console.warn('[LinkItBe] invalid integration payload for preview', e);
580
- return;
581
- }
582
- if (!integration.preview || !integration.preview.img) {
583
- return;
584
- }
585
- link.addEventListener('mouseenter', (e) => showPreview(preview, integration, e.clientX, e.clientY));
586
- link.addEventListener('mousemove', (e) => {
587
- preview.style.left = (e.clientX + 16) + 'px';
588
- preview.style.top = (e.clientY - 60) + 'px';
589
- });
590
- link.addEventListener('mouseleave', () => { preview.style.display = 'none'; });
591
- });
592
- }
593
-
594
- return { appliedCount, integrations: active };
595
- }
596
-
597
-
598
- // ============================================================
599
- // INIT
600
- // ============================================================
601
-
602
- async function init() {
603
- const articleId = getArticleId();
604
- if (!articleId) {
605
- console.warn('[LinkItBe] Article ID not found');
606
- return;
607
- }
608
-
609
- console.log('[LinkItBe] Article ID:', articleId);
610
-
611
- try {
612
- const data = await fetchMarkup(articleId);
613
- resolveConfig(data.config || {});
614
- const integrations = Array.isArray(data.integrations) ? data.integrations : [];
615
- const result = applyIntegrations(integrations);
616
- await emitViewRendered(articleId, result.integrations, result.appliedCount);
617
- } catch (e) {
618
- console.warn('[LinkItBe] failed to load markup', e);
619
- }
620
- }
621
-
622
- if (document.readyState === 'loading') {
623
- document.addEventListener('DOMContentLoaded', init);
624
- } else {
625
- init();
626
- }
627
- })();
1
+ !function(){const e=Object.assign({domain:window.location.hostname,bootOnLoad:!0,contentSelector:null,showPreview:!0,plainBlockJoiner:"\n",plainBlockSelector:"p, blockquote, h1, h2, h3, h4, h5, h6",allowedFormats:["keyword","mini-offer"],apiBaseUrl:"https://api.linkitbe.com",partnerApiKey:null,articleIdResolver:null,enableFeedAutoProcess:!1,feedObserverRootSelector:null,sdkVersion:"1.0.2",styles:{link_color:"#e2187b",link_weight:"700",link_decoration:"underline"}},window.LinkItBeConfig||{});function n(n){if("function"==typeof e.articleIdResolver)try{const t=e.articleIdResolver(n),o=null!=t?String(t).trim():"";if(o)return o}catch(e){console.warn("[LinkItBe] articleIdResolver failed",e)}return null}function t(){const e="__lb_session_id";try{const n=window.localStorage.getItem(e);if(n&&n.trim())return n.trim();const t=window.crypto&&window.crypto.randomUUID?window.crypto.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`;return window.localStorage.setItem(e,t),t}catch(e){return`${Date.now()}-${Math.random().toString(16).slice(2)}`}}function o(e){if(!Array.isArray(e))return null;for(const n of e){const e=n&&n.link?String(n.link):"";if(e)try{const n=new URL(e,window.location.origin).searchParams.get("processing_id"),t=Number(n);if(Number.isFinite(t)&&t>0)return t}catch(e){}}return null}async function r(n,r,i,l,s){const c=o([l]);if(!c)return;const a=e.partnerApiKey?String(e.partnerApiKey).trim():"";if(!a)return;const d=Object.assign({event_id:window.crypto&&window.crypto.randomUUID?window.crypto.randomUUID():`${Date.now()}-${Math.random().toString(16).slice(2)}`,event_type:n,event_time:(new Date).toISOString(),article_id:i,domain:e.domain,processing_id:c,placement_link:window.location.href,integration_type:l&&null!=l.type?l.type:null,sdk_version:String(e.sdkVersion||""),session_id:t(),referrer:document.referrer||null},s||{});try{const n=String(e.apiBaseUrl||"https://api.linkitbe.com").replace(/\/$/,"");await fetch(`${n}${r}`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json","X-Partner-Api-Key":a},body:JSON.stringify(d),keepalive:!0,credentials:"omit"})}catch(e){console.warn("[LinkItBe] failed to send "+n,e)}}async function i(e,n,t){if(t<=0)return;const i=function(e){if(!Array.isArray(e))return null;for(let n=0;n<e.length;n++){const t=e[n];if(t&&o([t]))return t}return null}(n);i&&await r("view_rendered","/v1/events/view-rendered",e,i,{integrations_count:t})}function l(n){const t=e.allowedFormats;return null!=t&&Array.isArray(t)?n.filter(e=>t.includes(e.type)):n}function s(e){const n=[],t=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode:e=>function(e){const n=e.nodeType===Node.TEXT_NODE?e.parentElement:e;return!!(n&&n.closest&&n.closest("a.__lb_link"))}(e)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT});let o;for(;o=t.nextNode();)n.push(o);return n}function c(e,n){if(!n)return[];const t=new RegExp(String(n).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");const o=[];let r;for(;null!==(r=t.exec(e));)o.push({start:r.index,end:r.index+r[0].length}),0===r[0].length&&t.lastIndex===r.index&&t.lastIndex++;return o}function a(e,n,t){const o=function(e){if(null==e)return null;try{const n=new URL(String(e),window.location.origin);return"http:"!==n.protocol&&"https:"!==n.protocol?null:n.toString()}catch(e){return null}}(e.link);if(!o)return null;const r=document.createElement("a");return r.href=o,r.target="_blank",r.rel="noopener noreferrer sponsored nofollow",r.className="__lb_link",r.setAttribute("data-lb-integration",encodeURIComponent(JSON.stringify(e))),r.style.cssText=t.replace(/\s+/g," ").trim(),r.textContent=n,r}function d(n,t){const{plain:o,spans:r}=function(n){const t=null!=e.plainBlockJoiner?e.plainBlockJoiner:"\n",o=e.plainBlockSelector||"p, blockquote, h1, h2, h3, h4, h5, h6",r=n.querySelectorAll(o),i=[];let l="";return r.forEach((e,n)=>{n>0&&(l+=t);const o=s(e);for(const e of o){const n=e.textContent;if(!n.length)continue;const t=l.length;l+=n,i.push({g0:t,g1:l.length,node:e})}}),{plain:l,spans:i}}(n),i=function(e,n){if(!e.length)return null;if(1===e.length)return e[0];const t=n||{},o=t.match_index;if(null!=o&&Number.isFinite(Number(o))){const n=Number(o);if(n>=0&&n<e.length)return e[n];console.warn("[LinkItBe] match_index out of range:",o,"n=",e.length)}const r=t.offset_start,i=t.offset_end;if(null!=r&&Number.isFinite(Number(r))){const n=Number(r),t=null!=i&&Number.isFinite(Number(i))?Number(i):null;let o=e[0],l=1/0;for(const r of e){const e=1e9*Math.abs(r.start-n)+(null!=t?Math.abs(r.end-t):0);e<l&&(l=e,o=r)}return o}return console.warn("[LinkItBe] ambiguous matches, skipping (no match_index / offset_start):",e.length),null}(c(o,t.keyword),t.position);if(!i)return null;const l=o.slice(i.start,i.end);return{plain:o,spans:r,span:i,linkText:l}}function u(e,n,t){const o=d(e,n);if(!o)return console.warn("[LinkItBe] no span for keyword:",n.keyword),!1;const{plain:r,spans:i,span:l,linkText:s}=o,c=function(e,n,t){if(e<0||e>=t||!n.length)return null;const o=n.find(n=>e>=n.g0&&e<n.g1);return o?{node:o.node,offset:e-o.g0}:null}(l.start,i,r.length),u=function(e,n,t){if(!n.length)return null;if(e<=0)return{node:n[0].node,offset:0};if(e>=t){const e=n[n.length-1];return{node:e.node,offset:e.node.textContent.length}}for(let t=0;t<n.length;t++){const o=n[t];if(e>o.g0&&e<=o.g1)return{node:o.node,offset:e-o.g0};if(e===o.g0)return{node:o.node,offset:0}}for(let t=0;t<n.length;t++)if(e<n[t].g0)return{node:n[t].node,offset:0};return null}(l.end,i,r.length);if(!c||!u)return console.warn("[LinkItBe] could not map span to DOM"),!1;const p=r.slice(l.start,l.end);if(p.toLowerCase()!==s.toLowerCase())return console.warn("[LinkItBe] plain slice mismatch"),!1;const f=document.createRange();f.setStart(c.node,c.offset),f.setEnd(u.node,u.offset);const g=a(n,p,t);return g?(function(e,n){try{e.surroundContents(n)}catch(t){const o=e.extractContents();for(;o.firstChild;)n.appendChild(o.firstChild);e.insertNode(n)}}(f,g),!0):(console.warn("[LinkItBe] invalid integration link:",n.link),!1)}const p="{anchor_text}";function f(n,t,o,i){if(!o){console.warn("[LinkItBe] Content block not provided");return{appliedCount:0,integrations:l(t)}}const s=e.showPreview?function(){const e=document.createElement("div");return e.id="__lb_preview",e.style.cssText="\n position: fixed;\n display: none;\n background: white;\n border-radius: 12px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.15);\n padding: 12px;\n z-index: 99999;\n width: 180px;\n font-family: Manrope, sans-serif;\n pointer-events: none;\n ",document.body.appendChild(e),e}():null,c=`\n color:${i.link_color};\n font-weight:${i.link_weight};\n text-decoration:${i.link_decoration};\n cursor:pointer;\n `,f=l(t);let g=0,h=f.filter(e=>"keyword"===e.type);for(;h.length;){const e=[];for(const n of h){const t=d(o,n);t&&e.push({integ:n,spanStart:t.span.start})}if(!e.length){h.forEach(e=>console.warn("[LinkItBe] unresolved keyword:",e.keyword));break}e.sort((e,n)=>n.spanStart-e.spanStart);const{integ:n}=e[0];u(o,n,c)&&(g+=1),h=h.filter(e=>e!==n)}return f.filter(e=>"mini-offer"===e.type).forEach(n=>{!function(n,t,o){const r=e.plainBlockSelector||"p, blockquote, h1, h2, h3, h4, h5, h6",i=n.querySelectorAll(r);if(!i.length)return console.warn("[LinkItBe] mini-offer: no content blocks"),!1;const l=i[i.length-1],s=document.createElement("p");s.className="__lb_mini_offer",s.style.cssText="margin-top:16px;font-size:15px;font-family:Manrope, sans-serif;color:#343b4c;";const c=document.createElement("span");c.style.cssText="font-size:11px;color:#999;display:block;margin-bottom:4px;",c.textContent="#реклама",s.appendChild(c);const d=document.createElement("span"),u=null!=t.text?String(t.text):"",f=null!=t.anchor_text?String(t.anchor_text):"",g=u.indexOf(p),h=g>=0?u.slice(0,g):u,w=g>=0?u.slice(g+13):"";h&&d.appendChild(document.createTextNode(h));const m=a(t,f,o);return m?(d.appendChild(m),w&&d.appendChild(document.createTextNode(w)),s.appendChild(d),l.after(s),!0):(console.warn("[LinkItBe] mini-offer invalid integration link:",t.link),!1)}(o,n,c)?console.warn("[LinkItBe] mini-offer failed"):g+=1}),s&&o.querySelectorAll(".__lb_link").forEach(e=>{let t;try{t=JSON.parse(decodeURIComponent(e.dataset.lbIntegration||""))}catch(e){return void console.warn("[LinkItBe] invalid integration payload for preview",e)}t.preview&&t.preview.img&&(e.addEventListener("mouseenter",o=>{!function(e,n,t,o,r){if(!n.preview||!n.preview.img)return void(e.style.display="none");e.replaceChildren();const i=document.createElement("img");i.src=String(n.preview.img),i.style.cssText="width:100%;border-radius:8px;display:block;",e.appendChild(i);const l=document.createElement("div");l.style.cssText="margin-top:8px;font-size:13px;color:#343b4c;font-weight:600;",l.textContent=n.preview.title?String(n.preview.title):"",e.appendChild(l);const s=document.createElement("div");s.style.cssText="font-size:12px;color:#777;margin-top:2px;",s.textContent=n.preview.price?String(n.preview.price):"",e.appendChild(s);const c=document.createElement("div");c.style.cssText=`font-size:12px;color:${r.link_color};margin-top:2px;`,c.textContent=n.preview.cta?String(n.preview.cta):"",e.appendChild(c),e.style.left=t+16+"px",e.style.top=o-60+"px",e.style.display="block"}(s,t,o.clientX,o.clientY,i),"1"!==e.dataset.lbPreviewSent&&n&&(e.dataset.lbPreviewSent="1",r("preview_viewed","/v1/events/preview-viewed",n,t))}),e.addEventListener("mousemove",e=>{s.style.left=e.clientX+16+"px",s.style.top=e.clientY-60+"px"}),e.addEventListener("mouseleave",()=>{s.style.display="none"}))}),function(e,n){if(!e)return;const t=new IntersectionObserver(n=>{for(let o=0;o<n.length;o++){const i=n[o];if(!i.isIntersecting||i.intersectionRatio<.5)continue;const l=i.target;if("1"===l.dataset.lbViewableSent)continue;let s;try{s=JSON.parse(decodeURIComponent(l.dataset.lbIntegration||""))}catch(e){t.unobserve(l);continue}l.dataset.lbViewableSent="1",r("integration_viewable","/v1/events/integration-viewable",e,s),t.unobserve(l)}},{threshold:[.5]});n.querySelectorAll(".__lb_link").forEach(e=>t.observe(e))}(n,o),{appliedCount:g,integrations:f}}async function g(n,t){if(!function(e,n){return e?!!n||(console.warn("[LinkItBe] Content block not found for article:",e),!1):(console.warn("[LinkItBe] Article ID not found"),!1)}(n,t))return;if("1"===t.dataset.lbProcessed)return;const o=await async function(n){const t=`${String(e.apiBaseUrl||"https://api.linkitbe.com").replace(/\/$/,"")}/v1/markup?article_id=${encodeURIComponent(n)}&domain=${encodeURIComponent(e.domain)}`,o={Accept:"application/json"},r=e.partnerApiKey?String(e.partnerApiKey).trim():"";r&&(o["X-Partner-Api-Key"]=r);const i=await fetch(t,{method:"GET",headers:o,credentials:"omit"});return i.ok?await i.json():null}(n);o&&await async function(e,n,t){if("1"===n.dataset.lbProcessed)return;n.dataset.lbProcessed="1";const o=f(e,Array.isArray(t.integrations)?t.integrations:[],n,t.config.style);await i(e,o.integrations,o.appliedCount)}(n,t,o)}function h(t){if(t.nodeType!==Node.ELEMENT_NODE)return;const o=t.querySelector(e.contentSelector);if(!o)return;g(n(o),o)}function w(e){if(!e)return void console.warn("[LinkItBe] feedRoot not provided for auto markup");new MutationObserver(e=>{for(const n of e)for(let e=0;e<n.addedNodes.length;e++)h(n.addedNodes[e])}).observe(e,{childList:!0,subtree:!0})}async function m(){const t=document.querySelector(e.contentSelector),o=n(t);if(await g(o,t),!0!==e.enableFeedAutoProcess)return;const r=document.querySelector(e.feedObserverRootSelector);r?w(r):console.warn("[LinkItBe] feedObserverRootSelector not found:",e.feedObserverRootSelector)}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",m):!0===e.bootOnLoad&&m(),window.LinkItBe=Object.assign({},window.LinkItBe||{},{initArticle:g,startFeedAutoMarkup:w})}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkitbe/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "LinkItBe browser SDK",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {