121 lines
6.6 KiB
JavaScript
121 lines
6.6 KiB
JavaScript
// 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); search, fetch, and image
|
|
// (a picture to make or - with edit - to change; the first line may give
|
|
// its ratio, quality and the photo's file name).
|
|
function parseActions(text) {
|
|
const out = [], re = /```(run|read|ls|write|search|fetch|image)([^\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 === 'image') {
|
|
// ```image [edit] [1:1|16:9|…|auto] [fast|artist|0.2plus] [noenhance] [file name]
|
|
let rest = head, edit = false, enhance = true, ratio = '', model = '', file = '', t;
|
|
if ((t = /^edit(?=\s|$)\s*/.exec(rest))) { edit = true; rest = rest.slice(t[0].length); }
|
|
rest = rest.replace(/(?:^|\s)(\d+:\d+|auto)(?=\s|$)/, (_, v) => { ratio = v; return ' '; })
|
|
.replace(/(?:^|\s)(fast|artist|0\.2plus)(?=\s|$)/, (_, v) => { model = v; return ' '; })
|
|
.replace(/(?:^|\s)(noenhance|enhance)(?=\s|$)/, (_, v) => { enhance = v === 'enhance'; return ' '; });
|
|
file = rest.trim();
|
|
out.push({kind, machine: '', path: '', body, edit, enhance, ratio, model, file});
|
|
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, '<b>$1</b>')
|
|
.replace(/(^|[^*\w])\*([^*\n]+)\*(?!\w)/g, '$1<i>$2</i>')
|
|
.replace(/(^|\W)_([^_\n]+)_(?!\w)/g, '$1<i>$2</i>')
|
|
.replace(/\[([^\]\n]+)\]\(((?:[^()\s]|\([^()\s]*\))+)\)/g, (_, txt, url) => /^https?:\/\//i.test(url.replace(/&/g, '&'))
|
|
? '<a href="' + url + '" target="_blank" rel="noopener">' + txt + '</a>' : txt);
|
|
return t.replace(/\u0000(\d+)\u0000/g, (_, i) => '<code>' + escH(codes[+i]) + '</code>');
|
|
}
|
|
function md(text) {
|
|
const out = [];
|
|
let para = [], list = null;
|
|
const flush = () => {
|
|
if (para.length) out.push('<p>' + para.map(mdInline).join('<br>') + '</p>');
|
|
para = [];
|
|
if (list) { out.push('<' + list.tag + '>' + list.items.map(x => '<li>' + mdInline(x) + '</li>').join('') + '</' + list.tag + '>'); 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('<h4>' + mdInline(m[1]) + '</h4>'); continue; }
|
|
if (/^\s*(-{3,}|\*{3,})\s*$/.test(line)) { flush(); out.push('<hr>'); continue; }
|
|
if ((m = /^>\s?(.*)$/.exec(line))) { flush(); out.push('<blockquote>' + mdInline(m[1]) + '</blockquote>'); 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);
|