Greasy Fork is available in English.
Утилита, которая сокращает текст темы, экономя ваше время на прочтение.
// ==UserScript==
// @name LZT Article Summarizer
// @namespace http://tampermonkey.net/
// @version 0.4
// @description Утилита, которая сокращает текст темы, экономя ваше время на прочтение.
// @author @dikaronok
// @match https://lolz.live/threads/*
// @match https://zelenka.guru/threads/*
// @match https://lolz.guru/threads/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=zelenka.guru
// @license MIT
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
const ICONS = {
close: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>',
clock: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
key: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>',
document: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>',
eye: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
eyeOff: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>',
settings: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
sparkles: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>'
};
const DEFAULT_SETTINGS = {
minTextLength: 200,
maxSummaryLength: 500,
maxKeyPoints: 5,
buttonColor: '#10b981',
summaryBgColor: '#0f1a14',
summaryTextColor: '#e5e7eb',
animationSpeed: 250,
wordsPerMinute: 200
};
const IMPORTANT_WORDS = [
'важно', 'главное', 'необходимо', 'ключевой', 'существенно',
'критически', 'спонсор', 'проект', 'результат', 'итог',
'вывод', 'следовательно', 'таким образом', 'поэтому',
'значит', 'суть', 'основа', 'цель', 'задача'
];
const KEY_INDICATORS = [
'важно', 'главное', 'необходимо', 'следует', 'нужно',
'основной', 'ключевой', 'рекомендуется', 'обратите внимание',
'в первую очередь', 'самое важное', 'существенно', 'критически'
];
const SKIP_TAGS = new Set(['IMG', 'SCRIPT', 'STYLE']);
const SKIP_CLASSES = new Set(['bbCodeBlock', 'messageTextEndMarker', 'lzt-summary-button']);
let settings = { ...DEFAULT_SETTINGS, ...GM_getValue('lztSummarizerSettings', {}) };
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const adjustColor = (hex, amount) => {
const num = parseInt(hex.replace('#', ''), 16);
const r = clamp(((num >> 16) & 0xFF) + amount, 0, 255);
const g = clamp(((num >> 8) & 0xFF) + amount, 0, 255);
const b = clamp((num & 0xFF) + amount, 0, 255);
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
};
const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const saveSettings = () => {
GM_setValue('lztSummarizerSettings', settings);
injectStyles();
};
const resetSettings = () => {
GM_setValue('lztSummarizerSettings', {});
settings = { ...DEFAULT_SETTINGS };
injectStyles();
};
const createEl = (tag, attrs = {}, children = []) => {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([k, v]) => {
if (k === 'className') el.className = v;
else if (k === 'innerHTML') el.innerHTML = v;
else if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
else el.setAttribute(k, v);
});
children.forEach(c => el.appendChild(typeof c === 'string' ? document.createTextNode(c) : c));
return el;
};
const animateShow = (overlay, panel) => {
requestAnimationFrame(() => {
overlay.style.opacity = '1';
panel.style.transform = 'translate(-50%, -50%) scale(1)';
panel.style.opacity = '1';
});
};
const animateHide = (overlay, panel, onComplete) => {
overlay.style.opacity = '0';
panel.style.transform = 'translate(-50%, -50%) scale(0.95)';
panel.style.opacity = '0';
setTimeout(onComplete, settings.animationSpeed);
};
const createSettingsMenu = () => {
const fields = [
{ id: 'minTextLength', label: 'Мин. длина текста', type: 'number' },
{ id: 'maxSummaryLength', label: 'Макс. длина резюме', type: 'number' },
{ id: 'maxKeyPoints', label: 'Макс. ключевых моментов', type: 'number' },
{ id: 'buttonColor', label: 'Цвет акцента', type: 'color' },
{ id: 'summaryBgColor', label: 'Фон резюме', type: 'color' },
{ id: 'summaryTextColor', label: 'Цвет текста', type: 'color' },
{ id: 'animationSpeed', label: 'Скорость анимации (мс)', type: 'number' }
];
const container = createEl('div');
const overlay = createEl('div', { className: 'lzt-overlay lzt-settings-overlay' });
const panel = createEl('div', { className: 'lzt-panel lzt-settings-panel' });
const header = createEl('div', { className: 'lzt-panel-header' });
header.appendChild(createEl('h2', { className: 'lzt-panel-title', innerHTML: `${ICONS.settings} Настройки` }));
const closeBtn = createEl('button', { className: 'lzt-btn lzt-btn-icon', innerHTML: ICONS.close });
header.appendChild(closeBtn);
panel.appendChild(header);
const form = createEl('div', { className: 'lzt-settings-form' });
fields.forEach(({ id, label, type }) => {
const group = createEl('div', { className: 'lzt-form-group' });
group.appendChild(createEl('label', { for: id }, [label]));
group.appendChild(createEl('input', { type, id, value: settings[id] }));
form.appendChild(group);
});
panel.appendChild(form);
const actions = createEl('div', { className: 'lzt-panel-actions' });
const saveBtn = createEl('button', { className: 'lzt-btn lzt-btn-primary' }, ['Сохранить']);
const resetBtn = createEl('button', { className: 'lzt-btn lzt-btn-warning' }, ['Сбросить']);
actions.append(saveBtn, resetBtn);
panel.appendChild(actions);
container.append(overlay, panel);
document.body.appendChild(container);
const close = () => animateHide(overlay, panel, () => container.remove());
closeBtn.onclick = close;
overlay.onclick = close;
saveBtn.onclick = () => {
fields.forEach(({ id, type }) => {
const val = document.getElementById(id).value;
settings[id] = type === 'number' ? parseInt(val, 10) : val;
});
saveSettings();
close();
};
resetBtn.onclick = () => {
if (confirm('Сбросить все настройки?')) {
resetSettings();
close();
}
};
requestAnimationFrame(() => animateShow(overlay, panel));
};
const extractText = (element) => {
const walk = (node) => {
if (node.nodeType === Node.TEXT_NODE) return node.textContent.trim() + ' ';
if (node.nodeType !== Node.ELEMENT_NODE) return '';
if (SKIP_TAGS.has(node.tagName) || [...node.classList].some(c => SKIP_CLASSES.has(c))) return '';
return [...node.childNodes].map(walk).join('');
};
return walk(element).replace(/\s+/g, ' ').trim();
};
const splitSentences = (text) => text.match(/[^.!?]+[.!?]+/g) || [];
const scoreSentence = (sentence, index, total, prevWords = []) => {
const words = sentence.toLowerCase().split(/\s+/);
let score = 0;
score += words.filter(w => IMPORTANT_WORDS.some(imp => w.includes(imp))).length * 3;
if (sentence.length > 50 && sentence.length < 200) score += 2;
if (/\d/.test(sentence)) score += 2;
if (/[%$€₽]/.test(sentence)) score += 2;
if (index < 3) score += 3;
if (index >= total - 3) score += 2;
score += words.filter(w => w.length > 3 && prevWords.includes(w)).length * 0.5;
return score;
};
const summarizeText = (text) => {
const normalized = text.replace(/\s+/g, ' ').trim();
const sentences = normalized.split(/(?<=[.!?])\s+(?=[A-ZА-ЯЁ])/);
if (!sentences.length) return '';
const scored = sentences.map((sentence, i) => ({
sentence,
score: scoreSentence(sentence, i, sentences.length, i > 0 ? sentences[i - 1].toLowerCase().split(/\s+/) : []),
index: i
}));
scored.sort((a, b) => b.score - a.score);
const selected = scored.slice(0, Math.ceil(sentences.length * 0.3)).sort((a, b) => a.index - b.index);
let summary = selected.map(s => s.sentence.trim()).join('\n\n');
if (summary.length > settings.maxSummaryLength) {
summary = summary.slice(0, settings.maxSummaryLength);
const lastDot = summary.lastIndexOf('.');
if (lastDot > 0) summary = summary.slice(0, lastDot + 1);
}
return summary;
};
const extractKeyPoints = (text) => {
const sentences = splitSentences(text);
const points = new Set();
sentences.forEach(s => {
const lower = s.toLowerCase();
const trimmed = s.trim();
if (KEY_INDICATORS.some(ind => lower.includes(ind)) ||
(s.length > 30 && s.length < 200 && /\d/.test(s)) ||
/^[•\-]/.test(trimmed)) {
points.add(trimmed);
}
});
return [...points].slice(0, settings.maxKeyPoints);
};
const formatTime = (text) => {
const words = text.split(/\s+/).length;
const mins = words / settings.wordsPerMinute;
return mins < 1 ? `${Math.ceil(mins * 60)} сек` : `${Math.ceil(mins)} мин`;
};
const buildSummaryHTML = (summary, keyPoints, origTime, summaryTime) => `
<div class="lzt-summary-header">
<span class="lzt-summary-icon">${ICONS.document}</span>
<span>Краткое содержание</span>
</div>
<div class="lzt-summary-body">${summary || 'Не удалось создать резюме.'}</div>
<div class="lzt-summary-meta">
${ICONS.clock}
<span>${origTime} → ${summaryTime}</span>
</div>
<div class="lzt-key-section">
<div class="lzt-key-header">${ICONS.key} Ключевые моменты</div>
${keyPoints.length
? keyPoints.map(p => `<div class="lzt-key-item">${p}</div>`).join('')
: '<div class="lzt-key-item lzt-muted">Ключевые моменты не найдены</div>'}
</div>
`;
const createPopup = (content) => {
const overlay = createEl('div', { className: 'lzt-overlay' });
const popup = createEl('div', { className: 'lzt-panel lzt-popup', innerHTML: `
<button class="lzt-popup-close">${ICONS.close}</button>
${content}
` });
document.body.append(overlay, popup);
const close = () => animateHide(overlay, popup, () => { overlay.remove(); popup.remove(); });
popup.querySelector('.lzt-popup-close').onclick = close;
overlay.onclick = close;
requestAnimationFrame(() => animateShow(overlay, popup));
};
const addSummaryButton = (post) => {
if (!post.classList.contains('firstPost') || post.querySelector('.lzt-summary-button')) return;
const content = post.querySelector('.messageContent');
if (!content) return;
const text = extractText(content);
if (text.length < settings.minTextLength) return;
const btn = createEl('button', { className: 'lzt-summary-button', innerHTML: `${ICONS.sparkles} <span>Показать резюме</span>` });
let summaryEl = null;
let processing = false;
const updateBtnState = (showSummary) => {
btn.innerHTML = showSummary
? `${ICONS.eyeOff} <span>Скрыть резюме</span>`
: `${ICONS.sparkles} <span>Показать резюме</span>`;
};
btn.onclick = (e) => {
e.preventDefault();
if (processing) return;
processing = true;
if (summaryEl) {
const visible = summaryEl.style.display !== 'none';
summaryEl.style.display = visible ? 'none' : 'block';
content.classList.toggle('lzt-hidden', !visible);
updateBtnState(!visible);
processing = false;
} else {
const summary = summarizeText(text);
const keyPoints = extractKeyPoints(text);
const origTime = formatTime(text);
const summaryTime = formatTime(summary);
summaryEl = createEl('div', {
className: 'lzt-summary',
innerHTML: buildSummaryHTML(summary, keyPoints, origTime, summaryTime)
});
content.after(summaryEl);
content.classList.add('lzt-hidden');
updateBtnState(true);
setTimeout(() => { processing = false; }, settings.animationSpeed);
}
};
content.before(btn);
requestAnimationFrame(() => btn.classList.add('lzt-visible'));
};
const summarizeMainArticle = () => {
const post = document.querySelector('li.message.firstPost');
const content = post?.querySelector('.messageContent');
if (!content) {
alert('Статья не найдена');
return;
}
const text = extractText(content);
const summary = summarizeText(text);
const keyPoints = extractKeyPoints(text);
createPopup(buildSummaryHTML(summary, keyPoints, formatTime(text), formatTime(summary)));
};
const injectStyles = () => {
const { buttonColor, summaryBgColor, summaryTextColor, animationSpeed } = settings;
const hoverColor = adjustColor(buttonColor, -15);
GM_addStyle(`
:root {
--lzt-primary: ${buttonColor};
--lzt-primary-hover: ${hoverColor};
--lzt-bg: ${summaryBgColor};
--lzt-text: ${summaryTextColor};
--lzt-speed: ${animationSpeed}ms;
--lzt-radius: 12px;
--lzt-shadow: 0 8px 32px rgba(0,0,0,0.3);
}
.lzt-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
backdrop-filter: blur(8px);
z-index: 99998;
opacity: 0;
transition: opacity var(--lzt-speed) ease;
}
.lzt-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.95);
background: linear-gradient(145deg, rgba(30,35,40,0.98), rgba(20,25,30,0.98));
border: 1px solid rgba(255,255,255,0.08);
border-radius: var(--lzt-radius);
box-shadow: var(--lzt-shadow);
z-index: 99999;
opacity: 0;
transition: all var(--lzt-speed) ease;
color: var(--lzt-text);
}
.lzt-settings-panel {
width: 90%;
max-width: 420px;
padding: 24px;
}
.lzt-popup {
width: 90%;
max-width: 700px;
max-height: 85vh;
overflow-y: auto;
padding: 28px;
}
.lzt-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.lzt-panel-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
margin: 0;
color: var(--lzt-primary);
}
.lzt-panel-title svg {
flex-shrink: 0
font-weight: 600;
margin: 0;
color: var(--lzt-primary);
}
.lzt-panel-title svg {
flex-shrink: 0;
}
.lzt-btn {
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all var(--lzt-speed) ease;
}
.lzt-btn-icon {
padding: 8px 12px;
background: rgba(255,255,255,0.05);
color: var(--lzt-text);
}
.lzt-btn-icon:hover {
background: rgba(255,255,255,0.1);
}
.lzt-btn-primary {
flex: 1;
padding: 12px;
background: var(--lzt-primary);
color: white;
}
.lzt-btn-primary:hover {
background: var(--lzt-primary-hover);
transform: translateY(-1px);
}
.lzt-btn-warning {
flex: 1;
padding: 12px;
background: rgba(245,158,11,0.15);
color: #f59e0b;
}
.lzt-btn-warning:hover {
background: rgba(245,158,11,0.25);
}
.lzt-settings-form {
display: grid;
gap: 14px;
margin-bottom: 20px;
}
.lzt-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.lzt-form-group label {
font-size: 13px;
color: rgba(255,255,255,0.7);
}
.lzt-form-group input {
padding: 10px 12px;
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
color: var(--lzt-text);
font-size: 14px;
transition: border-color var(--lzt-speed) ease;
}
.lzt-form-group input:focus {
outline: none;
border-color: var(--lzt-primary);
}
.lzt-form-group input[type="color"] {
height: 42px;
padding: 4px;
cursor: pointer;
}
.lzt-panel-actions {
display: flex;
gap: 10px;
}
.lzt-popup-close {
position: absolute;
top: 16px;
right: 16px;
background: rgba(255,255,255,0.05);
border: none;
border-radius: 8px;
padding: 8px 10px;
color: var(--lzt-text);
cursor: pointer;
transition: all var(--lzt-speed) ease;
}
.lzt-popup-close:hover {
background: rgba(255,255,255,0.1);
color: var(--lzt-primary);
}
.lzt-summary-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
background: linear-gradient(135deg, var(--lzt-primary), ${hoverColor});
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin: 12px 0;
opacity: 0;
transform: translateY(8px);
transition: all var(--lzt-speed) ease;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.lzt-summary-button svg {
flex-shrink: 0;
}
.lzt-summary-button.lzt-visible {
opacity: 1;
transform: translateY(0);
}
.lzt-summary-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.25);
}
.lzt-summary {
background: var(--lzt-bg);
border-left: 3px solid var(--lzt-primary);
border-radius: 0 var(--lzt-radius) var(--lzt-radius) 0;
padding: 20px 24px;
margin: 16px 0;
color: var(--lzt-text);
animation: lzt-slide-in var(--lzt-speed) ease;
}
@keyframes lzt-slide-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.lzt-summary-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
color: var(--lzt-primary);
margin-bottom: 14px;
}
.lzt-summary-icon {
display: flex;
}
.lzt-summary-body {
line-height: 1.7;
white-space: pre-line;
margin-bottom: 16px;
}
.lzt-summary-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
opacity: 0.7;
margin-bottom: 16px;
}
.lzt-key-section {
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 16px;
}
.lzt-key-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: var(--lzt-primary);
margin-bottom: 12px;
}
.lzt-key-item {
position: relative;
padding-left: 18px;
margin: 8px 0;
line-height: 1.5;
}
.lzt-key-item::before {
content: "";
position: absolute;
left: 0;
top: 8px;
width: 6px;
height: 6px;
background: var(--lzt-primary);
border-radius: 50%;
}
.lzt-key-item.lzt-muted {
opacity: 0.6;
}
.lzt-key-item.lzt-muted::before {
background: rgba(255,255,255,0.3);
}
.lzt-hidden {
height: 0 !important;
opacity: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
}
`);
};
const init = () => {
injectStyles();
GM_registerMenuCommand('Резюме статьи', debounce(summarizeMainArticle, 300));
GM_registerMenuCommand('Настройки', debounce(createSettingsMenu, 300));
const tryAddButton = () => {
const post = document.querySelector('li.message.firstPost');
if (post) addSummaryButton(post);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', tryAddButton);
} else {
tryAddButton();
}
new MutationObserver(mutations => {
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches?.('li.message.firstPost')) {
addSummaryButton(node);
}
});
});
}).observe(document.body, { childList: true, subtree: true });
};
init();
})();