import { buildArweaveDataUrl, validateArweaveTxId, validateSha256Hex } from './arweave-file-service.js'; const ATTACH_PREFIX = ']*)>\n?/u; export function escapeHtml(text) { return String(text || '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } export function formatBytes(bytes) { const value = Number(bytes || 0); if (!Number.isFinite(value) || value <= 0) return '0 B'; if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; return `${(value / (1024 * 1024)).toFixed(2)} MB`; } function normalizeName(name) { const clean = String(name || 'file') .replace(/[\u0000-\u001f\u007f]/g, '') .replace(/[\\/]/g, '_') .trim(); return clean || 'file'; } export function normalizeAttachment(input = {}) { const txId = String(input.ar || input.txId || '').trim(); const sha256Hex = String(input.sha256 || input.sha256Hex || '').trim().toLowerCase(); const size = Number(input.size || input.sizeBytes || 0); const name = normalizeName(input.name || input.fileName || 'file'); if (!validateArweaveTxId(txId)) throw new Error('Некорректный Transaction ID Arweave.'); if (!validateSha256Hex(sha256Hex)) throw new Error('Некорректный SHA-256 файла.'); if (!Number.isInteger(size) || size <= 0) throw new Error('Некорректный размер файла.'); return { v: 1, name, size, sha256: sha256Hex, ar: txId, uploadedAtMs: Number(input.uploadedAtMs || 0) || Date.now(), }; } export function buildAttachmentBlock(attachment) { const item = normalizeAttachment(attachment); const encodedName = encodeURIComponent(item.name); return ``; } export function composeMessageWithAttachments(text, attachments = []) { const cleanText = String(text || '').trim(); const rows = (Array.isArray(attachments) ? attachments : []).map(buildAttachmentBlock); if (!rows.length) return cleanText; return `${rows.join('\n')}${cleanText ? `\n${cleanText}` : ''}`; } function parseFields(rawFields) { const out = {}; String(rawFields || '').split(';').forEach((part) => { const eq = part.indexOf('='); if (eq <= 0) return; const key = part.slice(0, eq).trim(); const value = part.slice(eq + 1).trim(); if (key) out[key] = value; }); return out; } export function parseMessageAttachments(rawText) { let rest = String(rawText || ''); const attachments = []; while (rest.startsWith(ATTACH_PREFIX)) { const match = rest.match(ATTACH_BLOCK_RE); if (!match) break; const fields = parseFields(match[1]); try { attachments.push(normalizeAttachment({ name: decodeURIComponent(String(fields.name || 'file')), size: fields.size, sha256: fields.sha256, ar: fields.ar, })); } catch { // Битый attach-блок скрываем из UI, но не ломаем отображение текста. } rest = rest.slice(match[0].length); } return { attachments, text: rest.replace(/^\n+/u, ''), }; } export function createAttachmentListElement(attachments = [], { gateway = '', compact = false } = {}) { const items = Array.isArray(attachments) ? attachments : []; const wrap = document.createElement('div'); wrap.className = `message-attachments${compact ? ' message-attachments--compact' : ''}`; items.forEach((raw) => { let item; try { item = normalizeAttachment(raw); } catch { return; } const url = buildArweaveDataUrl({ gateway, txId: item.ar }); const link = document.createElement('a'); link.className = 'message-attachment-card'; link.href = url; link.target = '_blank'; link.rel = 'noopener'; link.title = 'Открыть файл в Arweave'; link.addEventListener('click', (event) => { event.stopPropagation(); }); link.innerHTML = ` ${escapeHtml(item.name)} ${escapeHtml(formatBytes(item.size))} · ${escapeHtml(item.ar)} `; wrap.append(link); }); return wrap; }