File size: 3,025 Bytes
39371ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b74c172
 
39371ea
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { canHighlight, editorLanguage } from './editor-language.mjs';

export function createEditorUI(element) {
  const input = element.querySelector('#editor'), highlight = element.querySelector('#editor-highlight');
  let path = '', revision = 0, timer, watchdog, worker, active = false, composing = false, queued;

  function syncScroll() {
    // Match the textarea's actual viewport, excluding either native scrollbar.
    highlight.style.width = input.clientWidth + 'px';
    highlight.style.height = input.clientHeight + 'px';
    highlight.scrollTop = input.scrollTop;
    highlight.scrollLeft = input.scrollLeft;
  }
  function plain() { element.classList.remove('is-highlighted'); }
  function stopWorker() {
    clearTimeout(watchdog);
    worker?.terminate(); worker = undefined; active = false;
  }
  function send() {
    if (active || !queued) return;
    const job = queued; queued = undefined;
    try {
      if (!worker) {
        worker = new Worker(new URL('./editor-highlight.worker.mjs', import.meta.url), { type: 'module' });
        worker.onmessage = ({ data }) => {
          clearTimeout(watchdog); active = false;
          if (data.id === revision && data.html !== null) {
            // Prism escapes file contents before adding its own token spans.
            highlight.innerHTML = data.html + '\n';
            syncScroll(); element.classList.add('is-highlighted');
          }
          send();
        };
        worker.onerror = () => { stopWorker(); queued = undefined; };
      }
      active = true;
      worker.postMessage(job);
      // A pathological regex can only occupy this worker, and is terminated.
      watchdog = setTimeout(() => { stopWorker(); send(); }, 1500);
    } catch { stopWorker(); }
  }
  function refresh(delay = 0) {
    revision++; clearTimeout(timer); queued = undefined; plain();
    const language = editorLanguage(path), text = input.value;
    element.dataset.language = language ?? 'plain';
    if (composing || !text || !language || !canHighlight(text)) return;
    const id = revision;
    timer = setTimeout(() => { queued = { id, text, language }; send(); }, delay);
  }
  input.addEventListener('input', () => refresh(80));
  input.addEventListener('compositionstart', () => { composing = true; refresh(); });
  input.addEventListener('compositionend', () => { composing = false; refresh(80); });
  input.addEventListener('scroll', syncScroll, { passive: true });
  const resize = new ResizeObserver(syncScroll); resize.observe(input);
  return {
    setFile(nextPath, text, placeholder = nextPath ? 'Empty file.' : 'Select a file to inspect or edit it.') {
      input.placeholder = placeholder;
      if (path === nextPath && input.value === text) return;
      const changed = path !== nextPath;
      path = nextPath;
      if (input.value !== text) input.value = text;
      if (changed) { input.scrollTop = 0; input.scrollLeft = 0; }
      refresh();
    },
    dispose() { clearTimeout(timer); stopWorker(); resize.disconnect(); },
  };
}