'use strict'; const CP1251 = (() => { let s = ''; for (let i = 0; i < 0x80; i++) s += String.fromCharCode(i); s += 'ЂЃ‚ѓ„…†‡€‰Љ‹ЊЌЋЏђ‘’“”•–—\u0098™љ›њќћџ'; s += '\u00A0ЎўЈ¤Ґ¦§Ё©Є«¬\u00AD®Ї'; s += '°±Ііґµ¶·ё№є»јЅѕї'; for (let i = 0x410; i <= 0x42F; i++) s += String.fromCharCode(i); for (let i = 0x430; i <= 0x44F; i++) s += String.fromCharCode(i); return s; })(); const CP1251_INDEX = (() => { const m = new Map(); for (let i = 0; i < CP1251.length; i++) { if (!m.has(CP1251[i])) m.set(CP1251[i], i); } return m; })(); function toCp1251Bytes(str) { const bytes = []; for (const ch of str) { const b = CP1251_INDEX.get(ch); if (b === undefined) { const cp = ch.codePointAt(0).toString(16).toUpperCase().padStart(4, '0'); throw new Error(`Символ ${JSON.stringify(ch)} (U+${cp}) не входит в Windows-1251`); } bytes.push(b); } return bytes; } function bytesToBits(bytes) { let out = ''; for (const b of bytes) out += b.toString(2).padStart(8, '0'); return out; } let nodeCounter = 0; class Node { constructor(freq, char = null, left = null, right = null) { this.freq = freq; this.char = char; this.left = left; this.right = right; this.order = nodeCounter++; } } function buildHuffman(freq) { let nodes = Object.entries(freq).map(([ch, f]) => new Node(f, ch)) .sort((a, b) => a.freq - b.freq || a.order - b.order); while (nodes.length > 1) { const a = nodes.shift(); const b = nodes.shift(); const merged = new Node(a.freq + b.freq, null, a, b); let i = 0; while (i < nodes.length && (nodes[i].freq < merged.freq || (nodes[i].freq === merged.freq && nodes[i].order < merged.order))) i++; nodes.splice(i, 0, merged); } return nodes[0] || null; } function buildCodes(root) { const codes = {}; function walk(node, prefix) { if (!node) return; if (node.char !== null) { codes[node.char] = prefix || '0'; return; } walk(node.left, prefix + '0'); walk(node.right, prefix + '1'); } walk(root, ''); return codes; } function encode(text, codes) { const out = []; for (const ch of text) { if (!(ch in codes)) throw new Error(`Нет кода для символа ${JSON.stringify(ch)}`); out.push(codes[ch]); } return out.join(''); } function countFreq(text) { const freq = {}; for (const ch of text) freq[ch] = (freq[ch] || 0) + 1; return freq; } function bytesNeeded(bitCount) { return Math.ceil(bitCount / 8); } function escapeHtml(s) { return s.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function reprChar(ch) { return JSON.stringify(ch); } function layoutTree(root) { const positions = new Map(); let counter = 0; function visit(node, depth) { if (!node) return; visit(node.left, depth + 1); positions.set(node, { x: counter++, y: depth }); visit(node.right, depth + 1); } visit(root, 0); return positions; } function collectChars(node) { if (node.char !== null) return new Set([node.char]); const s = new Set(); if (node.left) for (const c of collectChars(node.left)) s.add(c); if (node.right) for (const c of collectChars(node.right)) s.add(c); return s; } function innerLabel(node) { const chars = [...collectChars(node)].sort(); const s = chars.join(''); if (s.length <= 6) return s; return s.slice(0, 3) + '…' + s.slice(-2); } const SVG_NS = 'http://www.w3.org/2000/svg'; function renderTree(root) { const container = document.getElementById('treeContainer'); container.innerHTML = ''; if (!root) return; const positions = layoutTree(root); const NODE_W = 68, NODE_H = 50, GAP_X = 22, GAP_Y = 64, PAD = 30; let maxX = 0, maxY = 0; for (const p of positions.values()) { if (p.x > maxX) maxX = p.x; if (p.y > maxY) maxY = p.y; } const width = PAD * 2 + (maxX + 1) * NODE_W + maxX * GAP_X; const height = PAD * 2 + (maxY + 1) * NODE_H + maxY * GAP_Y; const svg = document.createElementNS(SVG_NS, 'svg'); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.setAttribute('width', width); svg.setAttribute('height', height); function pos(node) { const p = positions.get(node); return { x: PAD + p.x * (NODE_W + GAP_X) + NODE_W / 2, y: PAD + p.y * (NODE_H + GAP_Y) + NODE_H / 2 }; } for (const [node] of positions) { const a = pos(node); for (const [child, label] of [[node.left, '0'], [node.right, '1']]) { if (!child) continue; const b = pos(child); const line = document.createElementNS(SVG_NS, 'line'); line.setAttribute('x1', a.x); line.setAttribute('y1', a.y + NODE_H / 2); line.setAttribute('x2', b.x); line.setAttribute('y2', b.y - NODE_H / 2); line.setAttribute('stroke', '#3a4051'); line.setAttribute('stroke-width', '1.5'); svg.appendChild(line); const t = document.createElementNS(SVG_NS, 'text'); t.setAttribute('x', (a.x + b.x) / 2); t.setAttribute('y', (a.y + b.y) / 2); t.setAttribute('fill', '#6aa9ff'); t.setAttribute('font-size', '12'); t.setAttribute('font-family', 'monospace'); t.setAttribute('text-anchor', 'middle'); t.setAttribute('dominant-baseline', 'middle'); t.textContent = label; svg.appendChild(t); } } for (const [node] of positions) { const { x, y } = pos(node); const isLeaf = node.char !== null; const rect = document.createElementNS(SVG_NS, 'rect'); rect.setAttribute('x', x - NODE_W / 2); rect.setAttribute('y', y - NODE_H / 2); rect.setAttribute('width', NODE_W); rect.setAttribute('height', NODE_H); rect.setAttribute('rx', 8); rect.setAttribute('fill', isLeaf ? '#3a3320' : '#1e2b3f'); rect.setAttribute('stroke', isLeaf ? '#7a6a3a' : '#3a5a80'); rect.setAttribute('stroke-width', '1'); svg.appendChild(rect); const t1 = document.createElementNS(SVG_NS, 'text'); t1.setAttribute('x', x); t1.setAttribute('y', y - 6); t1.setAttribute('fill', isLeaf ? '#fde68a' : '#93c5fd'); t1.setAttribute('font-size', '13'); t1.setAttribute('font-family', 'monospace'); t1.setAttribute('text-anchor', 'middle'); t1.setAttribute('dominant-baseline', 'middle'); t1.textContent = isLeaf ? node.char : innerLabel(node); svg.appendChild(t1); const t2 = document.createElementNS(SVG_NS, 'text'); t2.setAttribute('x', x); t2.setAttribute('y', y + 12); t2.setAttribute('fill', '#8b93a7'); t2.setAttribute('font-size', '11'); t2.setAttribute('font-family', 'monospace'); t2.setAttribute('text-anchor', 'middle'); t2.setAttribute('dominant-baseline', 'middle'); t2.textContent = node.freq; svg.appendChild(t2); } container.appendChild(svg); } function initHaffman() { const LAWS = [ 'первый закон: Робот не может причинить вред человеку или своим бездействием допустить, чтобы человеку был причинён вред.', 'второй закон: Робот должен повиноваться всем приказам, которые даёт человек, кроме тех случаев, когда эти приказы противоречат Первому закону.', 'третий закон: Робот должен заботиться о своей безопасности в той мере, в которой это не противоречит Первому или Второму законам.' ]; document.getElementById('exampleBtn').addEventListener('click', () => { const randomLaw = LAWS[Math.floor(Math.random() * LAWS.length)]; textInput.value = randomLaw; encodeAction(); }); const textInput = document.getElementById('textInput'); const encodeBtn = document.getElementById('encodeBtn'); const clearBtn = document.getElementById('clearBtn'); const errorBox = document.getElementById('error'); const resultSec = document.getElementById('result'); const treeSec = document.getElementById('treeSection'); if (!textInput) return; function showError(msg) { errorBox.textContent = msg; errorBox.classList.remove('hidden'); } function hideError() { errorBox.classList.add('hidden'); } function resetOutput() { resultSec.classList.add('hidden'); treeSec.classList.add('hidden'); document.getElementById('treeContainer').innerHTML = ''; } function encodeAction() { hideError(); resetOutput(); const text = textInput.value; if (!text) { showError('Введите текст.'); return; } let originalByteArr; try { originalByteArr = toCp1251Bytes(text); } catch (e) { showError(e.message); return; } const originalBits = bytesToBits(originalByteArr); const originalBytes = originalByteArr.length; const freq = countFreq(text); nodeCounter = 0; const root = buildHuffman(freq); const codes = buildCodes(root); let encodedBits; try { encodedBits = encode(text, codes); } catch (e) { showError(e.message); return; } const encodedBytes = bytesNeeded(encodedBits.length); document.getElementById('originalBits').textContent = originalBits; document.getElementById('encodedBits').textContent = encodedBits; document.getElementById('statOrigBits').textContent = originalBits.length; document.getElementById('statOrigBytes').textContent = `${originalBytes} байт (CP1251)`; document.getElementById('statEncBits').textContent = encodedBits.length; document.getElementById('statEncBytes').textContent = `${encodedBytes} байт (с округлением)`; const ratio = encodedBytes / originalBytes * 100; const saved = originalBytes - encodedBytes; const savedPct = saved / originalBytes * 100; document.getElementById('statRatio').textContent = ratio.toFixed(1) + '%'; document.getElementById('statSaved').textContent = saved > 0 ? `экономия ${saved} байт (${savedPct.toFixed(1)}%)` : (saved === 0 ? 'без изменений' : `перерасход ${-saved} байт`); const tbody = document.querySelector('#freqTable tbody'); tbody.innerHTML = ''; const sorted = Object.entries(freq).sort((a, b) => { if (a[1] !== b[1]) return b[1] - a[1]; return a[0].localeCompare(b[0]); }); for (const [ch, f] of sorted) { const tr = document.createElement('tr'); tr.innerHTML = `${escapeHtml(reprChar(ch))}${f}${codes[ch]}`; tbody.appendChild(tr); } resultSec.classList.remove('hidden'); renderTree(root); treeSec.classList.remove('hidden'); } encodeBtn.addEventListener('click', encodeAction); clearBtn.addEventListener('click', () => { textInput.value = ''; hideError(); resetOutput(); }); document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); encodeAction(); } }); textInput.focus(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initHaffman); } else { initHaffman(); }