// Pure helpers of the workspace: quoting paths for a shell, what a dropped // file does where it lands, sizes. No DOM here, so node can test them. (function (root) { 'use strict'; function shellQuote(p) { return "'" + String(p).replace(/'/g, "'\\''") + "'"; } function shellQuoteAll(ps) { return ps.map(shellQuote).join(' '); } const same = (a, b) => a.kind === b.kind && a.id === b.id; // dropAction says what dropping item on target does: a transfer between // file panes, paths pasted into a terminal of the same place, nothing on // its own folder, or a refusal with the reason. function dropAction(item, target) { if (target.kind === 'term') { return same(item.source, target.source) ? {kind: 'paste'} : {kind: 'deny', reason: 'Нельзя: файл на ' + item.source.label + ', а терминал на ' + target.source.label}; } if (same(item.source, target.source) && target.dir === item.dir) return {kind: 'none'}; return {kind: 'transfer'}; } function fmtSize(n) { if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' K'; if (n < 1073741824) return (n / 1048576).toFixed(1) + ' M'; return (n / 1073741824).toFixed(1) + ' G'; } // scope gives a module timers, requests and listeners that end with it; // once ended it starts nothing new (a request's late answer cannot set a // timer going again). function scope() { let ended = false; const timers = new Set(), offs = [], ac = new AbortController(); return { timeout(fn, ms) { if (ended) return 0; const id = setTimeout(() => { timers.delete(id); fn(); }, ms); timers.add(id); return id; }, interval(fn, ms) { if (ended) return 0; const id = setInterval(fn, ms); timers.add(id); return id; }, clear(id) { clearTimeout(id); clearInterval(id); timers.delete(id); }, fetch(url, opt) { const o = Object.assign({credentials: 'same-origin'}, opt || {}); // A request with its own signal ends on either. o.signal = o.signal && AbortSignal.any ? AbortSignal.any([o.signal, ac.signal]) : (o.signal || ac.signal); return fetch(url, o); }, on(target, ev, fn, opt) { target.addEventListener(ev, fn, opt); offs.push(() => target.removeEventListener(ev, fn, opt)); }, signal: ac.signal, end() { ended = true; timers.forEach(id => { clearTimeout(id); clearInterval(id); }); timers.clear(); offs.forEach(f => f()); ac.abort(); } }; } // parseActions finds what an assistant's answer asks the panel to do: // closed ``` blocks headed run [machine], read|ls [machine:]path or // write [machine:]path (its body the file's text). function parseActions(text) { const out = [], re = /```(run|read|ls|write|search|fetch)([^\n]*)\n([\s\S]*?)```/g; for (let m; (m = re.exec(text));) { const kind = m[1], head = m[2].trim(), body = m[3].replace(/\n$/, ''); if (kind === 'run') { out.push({kind, machine: head, path: '', body}); continue; } if (kind === 'search' || kind === 'fetch') { out.push({kind, machine: '', path: '', body: (head + '\n' + body).trim()}); continue; } const c = head.indexOf(':'); const machine = c > 0 && !head.startsWith('/') ? head.slice(0, c) : ''; out.push({kind, machine, path: machine ? head.slice(c + 1) : head, body: kind === 'write' ? body : ''}); } return out; } // md turns an assistant's text (no ``` blocks - those are drawn apart) // into HTML: headings, lists, quotes, bold, italic, `code` and http(s) // links. Everything is escaped first, so nothing else gets through. const escH = v => String(v).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c])); function mdInline(t) { const codes = []; t = t.replace(/`([^`\n]+)`/g, (_, c) => { codes.push(c); return '\u0000' + (codes.length - 1) + '\u0000'; }); t = escH(t) .replace(/\*\*([^*\n]+)\*\*/g, '$1') .replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, '$1$2') .replace(/(^|\W)_([^_\n]+)_(?!\w)/g, '$1$2') .replace(/\[([^\]\n]+)\]\(((?:[^()\s]|\([^()\s]*\))+)\)/g, (_, txt, url) => /^https?:\/\//i.test(url.replace(/&/g, '&')) ? '' + txt + '' : txt); return t.replace(/\u0000(\d+)\u0000/g, (_, i) => '' + escH(codes[+i]) + ''); } function md(text) { const out = []; let para = [], list = null; const flush = () => { if (para.length) out.push('

' + para.map(mdInline).join('
') + '

'); para = []; if (list) { out.push('<' + list.tag + '>' + list.items.map(x => '
  • ' + mdInline(x) + '
  • ').join('') + ''); list = null; } }; for (const line of String(text).split('\n')) { let m; if (!line.trim()) { flush(); continue; } if ((m = /^#{1,6}\s+(.*)$/.exec(line))) { flush(); out.push('

    ' + mdInline(m[1]) + '

    '); continue; } if (/^\s*(-{3,}|\*{3,})\s*$/.test(line)) { flush(); out.push('
    '); continue; } if ((m = /^>\s?(.*)$/.exec(line))) { flush(); out.push('
    ' + mdInline(m[1]) + '
    '); continue; } const ul = /^\s*[-*•]\s+(.*)$/.exec(line), ol = /^\s*\d+[.)]\s+(.*)$/.exec(line); if (ul || ol) { const tag = ul ? 'ul' : 'ol'; if (para.length || (list && list.tag !== tag)) flush(); if (!list) list = {tag, items: []}; list.items.push((ul || ol)[1]); continue; } if (list) flush(); para.push(line); } flush(); return out.join(''); } const api = {shellQuote, shellQuoteAll, dropAction, fmtSize, scope, parseActions, md}; if (typeof module !== 'undefined') module.exports = api; else root.WSUtil = api; })(this);