@linkitbe/sdk 1.0.0
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.
- package/dist/linkitbe_sdk.min.js +627 -0
- package/package.json +15 -0
|
@@ -0,0 +1,627 @@
|
|
|
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
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linkitbe/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LinkItBe browser SDK",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"main": "dist/linkitbe_sdk.min.js",
|
|
13
|
+
"unpkg": "dist/linkitbe_sdk.min.js",
|
|
14
|
+
"jsdelivr": "dist/linkitbe_sdk.min.js"
|
|
15
|
+
}
|