';
}
function shortName(id) { var p = paras().filter(function (x) { return x.id === id; })[0]; return p ? p.num : id; }
function secNav(prev, next) {
var h = '
';
h += prev ? '' : '';
h += next ? '' : '';
h += '
'; return h;
}
function readBtn(id, label) {
return '';
}
document.addEventListener('click', function (e) {
var b = e.target.closest && e.target.closest('[data-read]'); if (!b) return;
var id = b.getAttribute('data-read');
addXp(10, id + '-read'); bumpProgress(id, 30);
b.textContent = 'Прочитано! +10 XP'; b.disabled = true; b.style.opacity = .6;
});
/* ============================================================ CONFETTI */
var _cc = null, _cp = [], _craf = null;
function confetti() {
try {
if (/jsdom/i.test(navigator.userAgent || '')) return; /* headless-guard: canvas в jsdom не реализован */
if (!_cc) { _cc = document.createElement('canvas'); _cc.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999'; document.body.appendChild(_cc); }
var c = _cc; c.width = window.innerWidth; c.height = window.innerHeight;
var ctx = c.getContext('2d'); if (!ctx) return;
var colors = ['#4f46e5', '#6366f1', '#0891b2', '#10b981', '#f59e0b', '#e11d48'];
for (var i = 0; i < 80; i++) _cp.push({ x: window.innerWidth / 2 + (Math.random() - .5) * 200, y: window.innerHeight / 2, vx: (Math.random() - .5) * 14, vy: -10 - Math.random() * 10, g: .4, life: 100, color: colors[i % colors.length], r: 4 + Math.random() * 4, rot: 0, vRot: (Math.random() - .5) * .3 });
if (_craf) cancelAnimationFrame(_craf);
function frame() { ctx.clearRect(0, 0, c.width, c.height); _cp = _cp.filter(function (p) { p.x += p.vx; p.y += p.vy; p.vy += p.g; p.life--; p.rot += p.vRot; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = p.color; ctx.fillRect(-p.r, -p.r / 2, p.r * 2, p.r); ctx.restore(); return p.life > 0 && p.y < c.height + 50; }); if (_cp.length > 0) _craf = requestAnimationFrame(frame); else { ctx.clearRect(0, 0, c.width, c.height); _craf = null; } }
frame();
} catch (e) {}
}
/* ============================================================ SORTER (DnD) */
function setupSorter(cfg) {
var placed = {}; var pool = document.getElementById(cfg.poolId); var scope = document.querySelector(cfg.scopeSelector);
if (!pool || !scope) return { placed: placed, render: function () {}, reset: function () {} };
pool.classList.add('dnd-pool'); if (cfg.columnLayout) pool.classList.add('col');
var armed = null;
function buildChip(it, isPlaced) { var e = document.createElement('div'); e.className = 'dnd-chip' + (isPlaced ? ' placed' : ''); e.dataset.id = it.id; e.innerHTML = '' + it.html + '×'; attach(e, it.id); return e; }
function attach(elm, itId) { elm.addEventListener('pointerdown', function (ev) { if (ev.button !== undefined && ev.button !== 0) return; ev.preventDefault(); if (ev.target.classList && ev.target.classList.contains('dnd-x')) { ev.stopPropagation(); if (placed[itId]) { delete placed[itId]; render(); } else if (armed === itId) { armed = null; render(); } return; } var sx = ev.clientX, sy = ev.clientY; var r = elm.getBoundingClientRect(); var ox = ev.clientX - r.left, oy = ev.clientY - r.top; var ghost = null, dragging = false; try { elm.setPointerCapture(ev.pointerId); } catch (e) {} function onMove(e2) { var dx = e2.clientX - sx, dy = e2.clientY - sy; if (!dragging && Math.hypot(dx, dy) > 8) { dragging = true; ghost = elm.cloneNode(true); ghost.style.cssText = 'position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:' + r.width + 'px;left:' + (e2.clientX - ox) + 'px;top:' + (e2.clientY - oy) + 'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if (dragging && ghost) { ghost.style.left = (e2.clientX - ox) + 'px'; ghost.style.top = (e2.clientY - oy) + 'px'; var under = document.elementsFromPoint(e2.clientX, e2.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); var tgt = under.find(function (n) { return n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')); }); if (tgt) tgt.classList.add('over'); } } function onUp(e2) { elm.removeEventListener('pointermove', onMove); elm.removeEventListener('pointerup', onUp); elm.removeEventListener('pointercancel', onUp); elm.classList.remove('dragging'); if (ghost) { ghost.remove(); ghost = null; } scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(function (n) { n.classList.remove('over'); }); if (dragging) { var under = document.elementsFromPoint(e2.clientX, e2.clientY); var box = under.find(function (n) { return n.classList && n.classList.contains('drop-box'); }); var pl = under.find(function (n) { return n.classList && n.classList.contains('dnd-pool'); }); if (box) { var di = box.querySelector('[data-cat]'); if (di) { placed[itId] = di.dataset.cat; armed = null; render(); return; } } else if (pl) { delete placed[itId]; armed = null; render(); return; } } else { if (placed[itId]) { delete placed[itId]; armed = null; render(); } else { armed = (armed === itId) ? null : itId; render(); } } dragging = false; } elm.addEventListener('pointermove', onMove); elm.addEventListener('pointerup', onUp); elm.addEventListener('pointercancel', onUp); }); }
function attachBoxTaps() { scope.querySelectorAll('.drop-box').forEach(function (box) { box.addEventListener('click', function (ev) { if (!armed) return; if (ev.target.closest('.dnd-chip')) return; var di = box.querySelector('[data-cat]'); if (di) { placed[armed] = di.dataset.cat; armed = null; render(); } }); }); }
function render() { pool.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id]) return; var c = buildChip(it, false); if (armed === it.id) c.classList.add('armed'); pool.appendChild(c); }); cfg.cats.forEach(function (cat) { var box = scope.querySelector('.drop-items[data-cat="' + cat + '"]'); if (!box) return; box.innerHTML = ''; cfg.items.forEach(function (it) { if (placed[it.id] !== cat) return; box.appendChild(buildChip(it, true)); }); }); if (window.renderMathInElement) try { renderMath(scope); } catch (e) {} }
attachBoxTaps(); render();
return { placed: placed, render: render, reset: function () { for (var k in placed) delete placed[k]; armed = null; render(); } };
}
/* ============================================================ GLOSSARY */
function wrapGlossary(root) {
var GL = M6.glossary || []; if (!root || root.__glossDone || !GL.length) return;
var allAliases = [];
GL.forEach(function (g, i) { (g.aliases || [g.term]).forEach(function (a) { allAliases.push({ a: a, i: i }); }); });
allAliases.sort(function (x, y) { return y.a.length - x.a.length; });
var re = new RegExp('(? cursor) out.appendChild(document.createTextNode(text.slice(cursor, m.index))); var found = m[0].toLowerCase(); var hit = allAliases.find(function (x) { return x.a.toLowerCase() === found; }); var g = hit ? GL[hit.i] : null; var sp = document.createElement('span'); sp.className = 'gloss-term'; sp.dataset.gloss = g ? g.term : ''; sp.textContent = m[0]; out.appendChild(sp); cursor = m.index + m[0].length; } if (cursor < text.length) out.appendChild(document.createTextNode(text.slice(cursor))); node.parentNode.replaceChild(out, node); });
root.__glossDone = true;
}
function initGlossaryTip() {
var tip = document.getElementById('gloss-tip'); if (!tip) return;
var GL = M6.glossary || []; var lockOpen = null;
function show(elm) { var g = GL.find(function (x) { return x.term === elm.dataset.gloss; }); if (!g) return; tip.innerHTML = '' + g.term[0].toUpperCase() + g.term.slice(1) + '
' + g.def + '
' + (g.sec ? '
См. ' + shortName(g.sec) + '
' : ''); if (window.renderMathInElement) renderMath(tip); var r = elm.getBoundingClientRect(); tip.classList.add('show'); var tw = tip.offsetWidth, th = tip.offsetHeight; var left = r.left, top = r.bottom + 8; if (left + tw > window.innerWidth - 12) left = window.innerWidth - tw - 12; if (top + th > window.innerHeight - 12) top = r.top - th - 8; tip.style.left = Math.max(8, left) + 'px'; tip.style.top = Math.max(8, top) + 'px'; }
function hide() { tip.classList.remove('show'); }
document.addEventListener('mouseover', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) show(elm); });
document.addEventListener('mouseout', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm && !lockOpen) hide(); });
document.addEventListener('click', function (e) { var elm = e.target.closest && e.target.closest('.gloss-term'); if (elm) { if (lockOpen === elm) { lockOpen = null; hide(); } else { lockOpen = elm; show(elm); } } else if (lockOpen && !e.target.closest('.gloss-tip')) { lockOpen = null; hide(); } });
}
/* ============================================================ SEARCH */
function buildSearchIndex() {
var arr = [];
paras().forEach(function (p) { arr.push({ kind: p.final ? 'Финал' : 'Параграф', title: p.num + ' ' + p.name, desc: p.sub || '', sec: p.id }); });
(M6.glossary || []).forEach(function (g) { arr.push({ kind: 'Понятие', title: g.term, desc: (g.def || '').replace(/\$/g, ''), sec: g.sec }); });
(M6.searchRows || []).forEach(function (r) { arr.push({ kind: r[0], title: r[1], desc: r[2], sec: r[3] }); });
return arr;
}
function initSearch() {
var modal = document.getElementById('search-modal'), inp = document.getElementById('search-input'), out = document.getElementById('search-results'), btn = document.getElementById('search-btn');
if (!modal || !inp || !out) return;
var INDEX = buildSearchIndex(); var cur = 0, rows = [];
function score(q, it) { var t = (it.title + ' ' + it.desc).toLowerCase(); if (t.includes(q)) return 100 + (it.title.toLowerCase().startsWith(q) ? 50 : 0); var s = 0; q.split(/\s+/).forEach(function (w) { if (w && t.includes(w)) s += 10; }); return s; }
function rank(q) { q = q.trim().toLowerCase(); if (!q) return INDEX.slice(0, 12); return INDEX.map(function (it) { return { it: it, s: score(q, it) }; }).filter(function (x) { return x.s > 0; }).sort(function (a, b) { return b.s - a.s; }).slice(0, 20).map(function (x) { return x.it; }); }
function render() { cur = 0; if (!rows.length) { out.innerHTML = '
Ничего не найдено
'; return; } out.innerHTML = rows.map(function (r, i) { return ''; }).join(''); out.querySelectorAll('.search-row').forEach(function (b) { b.addEventListener('click', function () { cur = +b.dataset.i; pick(); }); }); }
function pick() { var r = rows[cur]; if (!r) return; close(); goTo(r.sec); }
function move(d) { var items = out.querySelectorAll('.search-row'); if (!items.length) return; items[cur] && items[cur].classList.remove('active'); cur = (cur + d + items.length) % items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({ block: 'nearest' }); }
function open() { modal.classList.add('show'); inp.value = ''; rows = rank(''); render(); setTimeout(function () { inp.focus(); }, 50); }
function close() { modal.classList.remove('show'); }
btn && btn.addEventListener('click', open);
modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
inp.addEventListener('input', function () { rows = rank(inp.value); render(); });
inp.addEventListener('keydown', function (e) { if (e.key === 'ArrowDown') { e.preventDefault(); move(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); } else if (e.key === 'Enter') { e.preventDefault(); pick(); } else if (e.key === 'Escape') { e.preventDefault(); close(); } });
document.addEventListener('keydown', function (e) { if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { e.preventDefault(); if (modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle() {
var side = document.getElementById('col-side'), back = document.getElementById('col-side-backdrop'), btn = document.getElementById('sidebar-btn');
if (!side || !btn) return;
function open() { side.classList.add('open'); if (back) back.classList.add('show'); }
function close() { side.classList.remove('open'); if (back) back.classList.remove('show'); }
btn.addEventListener('click', function () { if (side.classList.contains('open')) close(); else open(); });
if (back) back.addEventListener('click', close);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
}
/* ============================================================ INIT */
function init() {
M6 = window.M6 || M6; /* перечитываем конфиг (порядок выполнения скриптов мог опередить объявление M6) */
loadProgress(); initTheme(); initSidebarToggle(); initGlossaryTip(); initSearch();
buildSections(); buildParaSelector(); refreshProgressUI(); loadServerReadState();
var first = (paras()[0] || {}).id; if (first) goTo(first);
if (M6.startAch) setTimeout(function () { achievement(M6.startAch[0], M6.startAch[1]); }, 600);
var foot = document.getElementById('m6-foot'); if (foot && M6.footer) foot.textContent = M6.footer;
if (window.LS && window.LS.xp) {
window.LS.xp.load().then(function (s) { if (s && s.xp > STATE.xp) { STATE.xp = s.xp; STATE.level = calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if (STATE.current) buildSidebar(STATE.current); } }).catch(function () {});
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
/* ============================================================ EXPORTS (для inline-билдеров) */
window.goTo = goTo;
window.makeCard = makeCard;
window.secNav = secNav;
window.readBtn = readBtn;
window.feedback = feedback;
window.renderMath = renderMath;
window.fmt = fmt; window.num = num;
window.addXp = addXp;
window.bumpProgress = bumpProgress;
window.achievement = achievement;
window.setupSorter = setupSorter;
window.confetti = confetti;
window.M6icon = M6icon;
window.M6engine = { goTo: goTo, ensureBuilt: ensureBuilt, refreshProgressUI: refreshProgressUI, buildSidebar: buildSidebar };
})();