https://github.com/quicwg/base-drafts
Raw File
Tip revision: fd884a603160575767f761acc47c6dec2c7c52ce authored by ID Bot on 28 March 2024, 00:48:39 UTC
Script updating archive at 2024-03-28T00:48:39Z. [ci skip]
Tip revision: fd884a6
issues.js
function setStatus(msg) {
  let status = document.getElementById('status');
  status.innerText = msg;
}

function date(s) {
  const d = Date.parse(s);
  if (isNaN(d)) {
    return 0;
  }
  return d;
}

function stateString(issue) {
  let str;
  if (issue.pr) {
    switch (issue.state) {
      case 'MERGED':
      str = 'merged';
      break;
      case 'CLOSED':
      str = 'discarded';
      break;
      default:
      str = 'pr';
      break;
    }
  } else {
    str = issue.state.toLowerCase();
  }
  return str;
}

function stateOrder(issue) {
  return ['open', 'pr', 'closed', 'merged', 'discarded'].indexOf(stateString(issue));
}

var sortKey = 'id';
var sortInvert = false;
function invert(x) {
  return x * (sortInvert ? -1 : 1);
}
function sort(k) {
  sortInvert = (k === sortKey) ? !sortInvert : false;
  k = k || sortKey;
  let message = k;
  switch (k) {
    case 'id':
      subset.sort((x, y) => invert(x.number - y.number));
      message = 'ID';
      break;
    case 'recent':
      subset.sort((x, y) => invert(date(y.updatedAt) - date(x.updatedAt)));
      message = 'last modified';
      break;
    case 'closed':
      subset.sort((x, y) => invert(date(y.closedAt) - date(x.closedAt)));
      message = 'time of closure';
      break;
    case 'title':
      subset.sort((x, y) => invert(x.title.localeCompare(y.title)));
      break;
    case 'state':
      subset.sort((x, y) => invert(stateOrder(x) - stateOrder(y)));
      break;
    case 'author':
      subset.sort((x, y) => invert(x.author.localeCompare(y.author)));
    break;
      default:
      setStatus('no idea how to sort like that');
      return;
  }
  setStatus(`sorted by ${message}${(sortInvert) ? ' (reversed)' : ''}`);
  sortKey = k;
  list(subset);
}

function sortSetup() {
  ['id', 'title', 'state', 'author'].forEach(k => {
    let el = document.getElementById(`sort-${k}`);
    el.addEventListener('click', _ => sort(k));
    el.style.cursor = 'pointer';
    el.title = `Sort by ${el.innerText}`;
  });
}

var db;
async function get() {
  db = null;
  const response = await fetch('archive.json');
  if (Math.floor(response.status / 100) !== 2) {
    throw new Error(`Error loading <${url}>: ${response.status}`);
  }
  db = await response.json();
  db.pulls ??= [];
  db.pulls.forEach(pr => pr.pr = true);
  subset = db.all = db.issues.concat(db.pulls);
  db.labels = db.labels.reduce((all, l) => {
    all[l.name] = l;
    return all;
  }, {});
  sort();
  document.title = `${db.repo} Issues`;
  console.log(`Loaded ${db.all.length} issues for ${db.repo}.`);
  console.log('Raw data for issues can be found in:');
  console.log('  db.all = all issues and pull requests');
  console.log('  subset = just the subset of issues that are shown');
  console.log('format(subset[, formatter]) to dump the current subset to the console');
}

var issueFilters = {
  assigned: {
    args: ['string'],
    h: 'assigned to this user',
    f: login => issue => {
      if (login === '') {
        return issue.assignees.length > 0;
      } else {
        return issue.assignees.some(assignee => assignee === login);
      }
    },
  },

  author: {
    args: ['string'],
    h: 'created by this user',
    f: login => issue => issue.author === login,
  },

  commenter: {
    args: ['string'],
    h: 'commented on by this user',
    f: login => issue => {
      return issue.author === login ||
        issue.comments.some(comment => comment.author === login) ||
        (issue.reviews || []).some(review => review.author === login);
    },
  },

  reviewer: {
    args: ['string'],
    h: 'reviewed by this user',
    f: login => issue => {
      return issue.reviews &&
        issue.reviews.some(review => review.author === login);
    },
  },

  user: {
    args: ['string'],
    h: 'mentions this user',
    f: login => issue => {
      return issue.author === login ||
        issue.assignees.some(assignee => assignee === login) ||
        issue.comments.some(comment => comment.author === login) ||
        (issue.reviews || []).some(review => review.author === login);
    },
  },

  closed: {
    args: [],
    h: 'is closed',
    f: issue => issue.state === 'CLOSED',
  },

  open: {
    args: [],
    h: 'is open',
    f: issue => issue.state === 'OPEN',
  },

  merged: {
    args: [],
    h: 'a merged pull request',
    f: issue => issue.state == 'MERGED',
  },

  discarded: {
    args: [],
    h: 'a discarded pull request',
    f: issue => issue.pr && issue.state === 'CLOSED'
  },

  n: {
    args: ['integer'],
    h: 'issue by number',
    f: i => issue => issue.number === i,
  },

  label: {
    args: ['string'],
    h: 'has a specific label',
    f: name => issue => issue.labels.some(label => label === name),
  },

  labelled: {
    args: [],
    h: 'has any label',
    f: issue => issue.labels.length > 0,
  },

  title: {
    args: ['string'],
    h: 'search title with a regular expression',
    f: function(re) {
      re = new RegExp(re);
      return issue => issue.title.match(re);
    }
  },

  body: {
    args: ['string'],
    h: 'search body with a regular expression',
    f: function(re) {
      re = new RegExp(re);
      return issue => issue.body.match(re);
    }
  },

  text: {
    args: ['string'],
    h: 'search title and body with a regular expression',
    f: function(re) {
      re = new RegExp(re);
      return issue => issue.title.match(re) || issue.body.match(re);
    }
  },

  pr: {
    args: [],
    h: 'is a pull request',
    f: issue => issue.pr,
  },

  issue: {
    args: [],
    h: 'is a plain issue, i.e., not(pr)',
    f: function(issue) {
      return !issue.pr;
    }
  },

  or: {
    args: ['filter', '...filter'],
    h: 'union',
    f: (...filters) =>  x => filters.some(filter => filter(x)),
  },

  and: {
    args: ['filter', '...filter'],
    h: 'intersection',
    f: (...filters) => x => filters.every(filter => filter(x)),
  },


  xor: {
    args: ['filter', '...filter'],
    h: 'for the insane',
    f: (...filters) =>
      x => filters.slice(1).reduce((a, filter) => a ^ filter(x), filters[0](x)),
  },

  not: {
    args: ['filter'],
    h: 'exclusion',
    f: a => issue => !a(issue),
  },

  closed_since: {
    args: ['date'],
    h: 'issues closed since the date and time',
    f: since => issue => date(issue.closedAt) >= since,
  },

  updated_since: {
    args: ['date'],
    h: 'issues updated since the date and time',
    f: since => issue => date(issue.updatedAt) >= since,
  }
};

class Parser {
  constructor(s) {
    this.str = s;
    this.skipws();
  }

  skipws() {
    this.str = this.str.trimLeft();
  }

  jump(idx) {
    this.str = this.str.slice(idx);
    this.skipws();
  }

  get next() {
    return this.str.charAt(0);
  }

  parseName() {
    let m = this.str.match(/^[a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?/);
    if (!m) {
      return;
    }

    this.jump(m[0].length);
    return m[0];
  }

  parseSeparator(separator) {
    if (this.next !== separator) {
      throw new Error(`Expecting separator ${separator}`);
    }
    this.jump(1);
  }

  parseString() {
    let end = -1;
    this.skipws();

    let bs = false;
    let quot = this.next === '"' || this.next === '\'';
    let quotchar = this.next;
    if (quot) { this.jump(1); }

    for (let i = 0; i < this.str.length; ++i) {
      let v = this.str.charAt(i);
      if (bs) {
        bs = false;
        continue;
      }
      if (v === '\\') {
        bs = true;
        continue;
      }
      if ((quot && v === quotchar) ||
          (!quot && (v === ')' || v === ','))) {
        end = i;
        break;
      }
    }
    if (end < 0) {
      throw new Error(`Unterminated string`);
    }
    let s = this.str.slice(0, end).trim();
    this.jump(end + (quot ? 1 : 0));
    return s.replace(/\\([\\"'])/g, '$1');
  }

  parseDate() {
    let str = this.parseString();
    let time = Date.parse(str);
    if (isNaN(time)) {
      throw new Error(`not a valid date: ${str}`);
    }
    return time;
  }

  parseNumber() {
    let m = this.str.match(/^\d+/);
    if (!m) {
      return;
    }
    this.jump(m[0].length);
    return parseInt(m[0], 10);
  }

  parseFilter() {
    if (this.next === '-') {
      this.parseSeparator('-');
      return issueFilters.not.f.call(null, this.parseFilter());
    }
    let name = this.parseName();
    if (!name) {
      let n = this.parseNumber();
      if (!isNaN(n)) {
        return issueFilters.n.f.call(null, n);
      }
      return;
    }
    let f = issueFilters[name];
    if (!f) {
      throw new Error(`Unknown filter: ${name}`);
    }
    if (f.args.length === 0) {
      return f.f;
    }
    let args = [];
    for (let i = 0; i < f.args.length; ++i) {
      let arg = f.args[i];
      let ellipsis = arg.slice(0, 3) === '...';
      if (ellipsis) {
        arg = arg.slice(3);
      }

      this.parseSeparator((i === 0) ? '(' : ',');
      if (arg === 'string') {
        args.push(this.parseString());
      } else if (arg === 'date') {
        args.push(this.parseDate());
      } else if (arg === 'integer') {
        args.push(this.parseNumber());
      } else if (arg === 'filter') {
        args.push(this.parseFilter());
      } else {
        throw new Error(`Error in filter ${name} definition`);
      }
      if (ellipsis && this.next === ',') {
        --i;
      }
    }
    this.parseSeparator(')');
    return f.f.apply(null, args);
  }
}

var subset = [];
function filterIssues(str) {
  subset = db.all;
  let parser = new Parser(str);
  let f = parser.parseFilter();
  while (f) {
    subset = subset.filter(f);
    f = parser.parseFilter();
  }
}

var formatter = {
  brief: x => `* ${x.title} (#${x.number})`,
  md: x => `* [#${x.number}](${x.url}): ${x.title}`,
};

function format(set, f) {
  return (set || subset).map(f || formatter.brief).join('\n');
}

var debounces = {};
var debounceSlowdown = 100;
function measureSlowdown() {
  let start = Date.now();
  window.setTimeout(_ => {
    let diff = Date.now() - start;
    if (diff > debounceSlowdown) {
      console.log(`slowed to ${diff} ms`);
      debounceSlowdown = Math.min(1000, diff + debounceSlowdown / 2);
    }
  }, 0);
}
function debounce(f) {
  let r = now => {
    measureSlowdown();
    f(now);
  };
  return e => {
    if (debounces[f.name]) {
      window.clearTimeout(debounces[f.name]);
      delete debounces[f.name];
    }
    if (e.key === 'Enter') {
      r(true);
    } else {
      debounces[f.name] = window.setTimeout(_ => {
        delete debounces[f.name];
        r(false)
      }, 10 + debounceSlowdown);
    }
  }
}

function cell(row, children, cellClass) {
  let td = document.createElement('td');
  if (cellClass) {
    td.className = cellClass;
  }
  if (Array.isArray(children)) {
    children.forEach(c => {
      td.appendChild(c);
      td.appendChild(document.createTextNode(' '));
    });
  } else {
    td.appendChild(children);
  }
  row.appendChild(td);
}


function loadAvatars(elements) {
  elements.forEach(e => {
    let avatar = new Image(16, 16);
    avatar.addEventListener('load', _ => e.target.replaceWith(avatar));
    let user = e.target.dataset.user;
    avatar.src = `https://github.com/${user}.png?size=16`;
  });
}
var intersection = new IntersectionObserver(loadAvatars, { rootMargin: '50px 0px 100px 0px' });

function author(x, click, userSearch) {
  let user = x.author || x;
  let sp = document.createElement('span');
  sp.classList.add('item', 'user');
  let ai = document.createElement('a');
  ai.href = `https://github.com/${user}`;
  ai.className = 'avatar';
  let placeholder = document.createElement('span');
  placeholder.className = 'swatch';
  placeholder.innerText = '\uD83E\uDDD0';
  placeholder.dataset.user = user;
  intersection.observe(placeholder);
  ai.appendChild(placeholder);
  sp.appendChild(ai);

  let au = document.createElement('a');
  au.href = `#${userSearch || 'user'}(${user})`;
  au.innerText = user;
  au.addEventListener('click', click);
  sp.appendChild(au);
  return sp;
}

function issueState(issue, click) {
  let st = document.createElement('span');
  st.classList.add('item', 'state');
  let a = document.createElement('a');
  a.innerText = stateString(issue);
  a.href = `#${stateString(issue)}`;
  if (click) {
    a.addEventListener('click', click);
  }
  st.appendChild(a);
  return st;
}

function showBody(item) {
  let div = document.createElement('div');
  div.className = 'body';
  let body = item.body.trim().replace(/\r\n?/g, '\n');

  let list = null;
  let el = null;
  let pre = null;
  function closeElement() {
    if (el) {
      if (list) {
        list.appendChild(el);
      } else {
        div.appendChild(el);
      }
    }
    el = null;
    pre = null;
  }
  function closeBoth() {
    closeElement();
    if (list) {
      div.appendChild(list);
      list = null;
    }
  }
  function addText(t) {
    if (pre) {
      el.appendChild(document.createTextNode(t + '\n'));
      return;
    }
    if (el.innerText !== '') {
      el.appendChild(document.createElement('br'));
    }
    if (t !== '') {
      el.appendChild(document.createTextNode(t));
    }
  }

  body.split('\n').forEach(t => {
    if (t.charAt(0) === ' ') {
      t = t.substring(1); // This fixes lots of problems.
    }
    if (t.indexOf('```') === 0) {
      let needNew = !el || !pre;
      closeBoth();
      if (needNew) {
        el = document.createElement('pre');
        pre = 'q';
        let language = t.substring(3).trim();
        if (language) {
          el.dataset.language = language;
        }
      }
    } else if (pre === 'q') {
      addText(t);
    } else if (!el && t.indexOf('   ') === 0) {
      if (!pre) {
        closeBoth();
        el = document.createElement('pre');
        pre = 's';
      }
      addText(t.substring(3));
    } else if (t.trim() === '') {
      closeElement();
    } else if (t.indexOf('# ') === 0) {
      closeBoth();
      el = document.createElement('h2');
      addText(t.substring(2).trimLeft());
      closeElement();
    } else if (t.indexOf('## ') === 0) {
      closeBoth();
      el = document.createElement('h3');
      addText(t.substring(3).trimLeft());
      closeElement();
    } else if (t.indexOf('### ') === 0) {
      closeBoth();
      el = document.createElement('h4');
      addText(t.substring(4).trimLeft());
      closeElement();
    } else if (t.charAt(0) === '>') {
      if (!el || el.tagName !== 'BLOCKQUOTE') {
        closeElement();
        el = document.createElement('blockquote');
      }
      addText(t.substring(1).trimLeft());
    } else if (t.indexOf('* ') === 0 || t.indexOf('- ') === 0) {
      if (list && list.tagName !== 'UL') {
        closeBoth();
      } else {
        closeElement();
      }
      if (!list) {
        list = document.createElement('ul');
      }
      el = document.createElement('li');
      addText(t.substring(2).trimLeft());
    } else if (t.match(/^(?:\(?\d+\)|\d+\.)/)) {
      if (list && list.tagName !== 'OL') {
        closeBoth();
      } else {
        closeElement();
      }
      if (!list) {
        list = document.createElement('ol');
      }
      el = document.createElement('li');
      let sep = t.match(/^(?:\(?\d+\)|\d+\.)/)[0].length;
      addText(t.substring(sep).trimLeft());
    } else {
      if (list && !el) {
        div.appendChild(list);
        list = null;
      }
      if (!el) {
        el = document.createElement('p');
      }
      addText(t);
    }
  });
  closeBoth();
  return div;
}

function showDate(d, reference) {
  let de = document.createElement('span');
  de.classList.add('item', 'date');
  const full = d.toISOString();
  const parts = full.split(/[TZ\.]/);
  if (reference && parts[0] === reference.toISOString().split('T')[0]) {
    de.innerText = parts[1];
  } else {
    de.innerText = parts[0] + ' ' + parts[1];
  }
  de.title = full;
  return de;
}

function narrow(e, extra) {
  e.preventDefault();
  hideIssue();
  let cmd = document.getElementById('cmd');
  let v = `${cmd.value} ${extra}`;
  cmd.value = v.trim();
  redraw(true);
}

function narrowLabel(e) {
  narrow(e, `label(${e.target.innerText})`);
}

function narrowState(e) {
  narrow(e, e.target.innerText);
}

function narrowUser(userType) {
  return function narrowUserInner(e) {
    narrow(e, `${userType}(${e.target.innerText})`);
  };
}

function showLabels(labels, click) {
  return labels.map(label => {
    let item = document.createElement('span');
    item.className = 'item';
    let sp = document.createElement('span');
    sp.className = 'swatch';
    item.appendChild(sp);
    let a = document.createElement('a');
    a.innerText = label;
    a.href = `#label(${label})`;
    if (click) {
      a.addEventListener('click', click);
    }
    if (db.labels.hasOwnProperty(label)) {
      sp.style.backgroundColor = '#' + db.labels[label].color;
      if (db.labels[label].description) {
        item.title = db.labels[label].description;
      }
    }
    item.appendChild(a);
    return item;
  });
}

// Make a fresh replacement element for the identified element.
function freshReplacement(id) {
  let e = document.getElementById(id);
  let r = document.createElement(e.tagName);
  r.id = id;
  e.replaceWith(r);
  return r;
}

var displayed = null;

function show(index) {
  if (index < 0 || index >= subset.length) {
    hideIssue();
    return;
  }
  displayed = index;
  const issue = subset[index];

  document.getElementById('overlay').classList.add('active');
  let frame = freshReplacement('issue');
  frame.classList.add('active');

  function showTitle() {
    let title = document.createElement('h2');
    title.className = 'title';
    let number = document.createElement('a');
    number.className = 'number';
    number.href = issue.url;
    number.innerText = `#${issue.number}`;
    title.appendChild(number);
    title.appendChild(document.createTextNode(': '));
    let name = document.createElement('a');
    name.href = issue.url;
    name.innerText = issue.title;
    title.appendChild(name);
    return title;
  }

  function showIssueLabels() {
    let meta = document.createElement('div');
    meta.className = 'meta';
    showLabels(issue.labels, hideIssue).forEach(el => {
      meta.appendChild(el);
      meta.appendChild(document.createTextNode(' '));
    });
    return meta;
  }

  function showIssueUsers() {
    let meta = document.createElement('div');
    meta.className = 'meta';
    meta.appendChild(author(issue, hideIssue, 'author'));
    if (issue.assignees && issue.assignees.length > 0) {
      let arrow = document.createElement('span');
      arrow.innerText = ' \u279c';
      arrow.title = 'Assigned to';
      meta.appendChild(arrow);
      issue.assignees.map(u => author(u, hideIssue, 'assigned')).forEach(el => {
        meta.appendChild(document.createTextNode(' '));
        meta.appendChild(el);
      });
    }
    return meta;
  }

  function showIssueDates() {
    let meta = document.createElement('div');
    meta.className = 'meta';
    let created = new Date(issue.createdAt);
    meta.appendChild(showDate(created));
    meta.appendChild(issueState(issue, hideIssue));
    if (issue.closedAt) {
      meta.appendChild(showDate(new Date(issue.closedAt), created));
    }
    return meta;
  }

  let refdate = null;
  function showComment(c) {
    let row = document.createElement('tr');
    let cdate = new Date(c.createdAt);
    cell(row, showDate(cdate, refdate), 'date');
    refdate = cdate;
    cell(row, author(c, hideIssue, (c.commit) ? 'reviewer' : 'commenter'), 'user');

    if (issue.pr) {
      let icon = document.createElement('span');
      switch (c.state) {
        case 'APPROVED':
          icon.innerText = '\u2714';
          icon.title = 'Approved';
          break;
        case 'CHANGES_REQUESTED':
          icon.innerText = '\u2718';
          icon.title = 'Changes Requested';
          break;
        default:
          icon.innerText = '\uD83D\uDCAC';
          icon.title = 'Comment';
          break;
      }
      cell(row, icon);
    }

    let body = showBody(c);
    if (c.comments && c.comments.length > 0) {
      let codeComments = document.createElement('div');
      codeComments.className = 'item';
      const s = (c.comments.length === 1) ? '' : 's';
      codeComments.innerText = `... ${c.comments.length} comment${s} on changes`;
      body.appendChild(codeComments);
    }
    cell(row, body);
    return row;
  }

  frame.appendChild(showTitle());
  frame.appendChild(showIssueLabels());
  frame.appendChild(showIssueUsers());
  frame.appendChild(showIssueDates());
  frame.appendChild(showBody(issue));

  let allcomments = (issue.comments || []).concat(issue.reviews || []);
  allcomments.sort((a, b) => date(a.createdAt) - date(b.createdAt));
  let comments = document.createElement('table');
  comments.className = 'comments';
  allcomments.map(showComment).forEach(row => comments.appendChild(row));
  frame.appendChild(comments);

  frame.scroll(0, 0);
  frame.focus();
}

function hideIssue() {
  document.getElementById('help').classList.remove('active');
  document.getElementById('issue').classList.remove('active');
  document.getElementById('overlay').classList.remove('active');
  displayed = null;
}

function step(n) {
  if (displayed === null) {
    if (n > 0) {
      show(n - 1);
    } else {
      show(subset.length + n);
    }
  } else {
    show(displayed + n);
  }
}

function makeRow(issue, index) {
  function cellID() {
    let a = document.createElement('a');
    a.innerText = issue.number;
    a.href = issue.url;
    a.onclick = e => {
      e.preventDefault();
      show(index);
    };
    return a;
  }

  function cellTitle() {
    let a = document.createElement('a');
    a.innerText = issue.title;
    a.href = issue.url;
    a.onclick = e => {
      e.preventDefault();
      show(index);
    };
    return a;
  }

  let tr = document.createElement('tr');
  cell(tr, cellID(), 'id');
  cell(tr, cellTitle(), 'title');
  cell(tr, issueState(issue, narrowState), 'state');
  cell(tr, author(issue, narrowUser('author'), 'author'), 'user');
  cell(tr, (issue.assignees || [])
             .map(u => author(u, narrowUser('assigned'), 'assigned')), 'assignees');
  cell(tr, showLabels(issue.labels, narrowLabel), 'labels');
  return tr;
}

function list(issues) {
  if (!issues) {
    return;
  }

  let body = freshReplacement('issuelist');
  body.innerHTML = '';
  issues.forEach((issue, index) => {
    body.appendChild(makeRow(issue, index));
  });
}

var currentFilter = '';
function filter(str, now) {
  try {
    filterIssues(str);
    setStatus(`${subset.length} records selected`);
    if (now) {
      window.location.hash = str;
      currentFilter = str;
    }
  } catch (e) {
    if (now) { // Only show errors when someone hits enter.
      setStatus(`Error: ${e.message}`);
      console.log(e);
    }
  }
}

function showHelp() {
  setStatus('help shown');
  let h = document.getElementById('help');
  h.classList.add('active');
  h.scroll(0, 0);
  h.focus();
  document.getElementById('overlay').classList.add('active');
}

function slashCmd(cmd) {
  if (cmd[0] === 'help') {
    document.getElementById('cmd').blur();
    showHelp();
  } else {
    setStatus('unknown command: /' + cmd.join(' '));
  }
}

function redraw(now) {
  let cmd = document.getElementById('cmd');
  if (cmd.value.charAt(0) == '/') {
    if (now) {
      slashCmd(cmd.value.slice(1).split(' ').map(x => x.trim()));
      cmd.value = currentFilter;
    }
    return;
  }

  if (!db) {
    if (now) {
      showStatus('Still loading...');
    }
    return;
  }

  document.getElementById('help').classList.remove('active');
  filter(cmd.value, now);
  list(subset);
}

function generateHelp() {
  let functionhelp = document.getElementById('functions');
  Object.keys(issueFilters).forEach(k => {
    let li = document.createElement('li');
    let arglist = '';
    if (issueFilters[k].args.length > 0) {
      arglist = '(' + issueFilters[k].args.map(x => '<' + x + '>').join(', ') + ')';
    }
    let fn = document.createElement('tt');
    fn.innerText = k + arglist;
    li.appendChild(fn);
    let help = '';
    if (issueFilters[k].h) {
      help = ' - ' + issueFilters[k].h;
    }
    li.appendChild(document.createTextNode(help));
    functionhelp.appendChild(li);
  });
}

function addFileHelp() {
  setStatus('error loading file');
  if (window.location.protocol !== 'file:') {
    return;
  }
  let p = document.createElement('p');
  p.className = 'warning';
  p.innerHTML = 'Important: Browsers display files inconsistently.' +
    ' You can work around this by running an HTTP server,' +
    ' such as <code>python3 -m http.server</code>,' +
    ' then view this file using that server.';
  document.getElementById('help').insertBefore(p, h.firstChild);
}

function issueOverlaySetup() {
  let overlay = document.getElementById('overlay');
  overlay.addEventListener('click', hideIssue);
  window.addEventListener('keyup', e => {
    if (e.target.id === 'cmd') {
      if (e.key === 'Escape') {
        e.preventDefault();
        e.target.blur();
      }
      return;
    }
    if (e.key === 'Escape') {
      e.preventDefault();
      hideIssue();
    }
  });
  window.addEventListener('keypress', e => {
    if (e.target.closest('input')) {
      return;
    }
    if (e.key === 'p' || e.key === 'k') {
      e.preventDefault();
      step(-1);
    } else if (e.key === 'n' || e.key === 'j') {
      e.preventDefault();
      step(1);
    } else if (e.key === '?') {
      e.preventDefault();
      showHelp();
    } else if (e.key === '\'') {
      e.preventDefault();
      hideIssue();
      document.getElementById('cmd').focus();
    } else if (e.key === 'c') {
      e.preventDefault();
      hideIssue();
      document.getElementById('cmd').value = '';
      redraw(true);
    }
  })
}

window.onload = () => {
  let cmd = document.getElementById('cmd');
  let redrawHandler = debounce(redraw);
  cmd.addEventListener('input', redrawHandler);
  cmd.addEventListener('keypress', redrawHandler);
  window.addEventListener('hashchange', e => {
    cmd.value = decodeURIComponent(window.location.hash.substring(1));
    redrawHandler(e);
  });
  if (window.location.hash) {
    cmd.value = decodeURIComponent(window.location.hash.substring(1));
  }
  sortSetup();
  generateHelp();
  issueOverlaySetup();
  get().then(redraw).catch(addFileHelp);
}
back to top