All files / src utils.js

35.71% Statements 65/182
25% Branches 29/116
30.19% Functions 16/53
39.49% Lines 62/157

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 4071x 1x 1x         1x 1x 1x 1x 1x               1x   118x                 484x 484x 484x 484x 8594x 8594x 8594x   484x         282x 282x 282x       83x       83x                                                             51x                   2x 2x 2x     2x                                     1x 1x         21x 21x 21x 21x 21x 8x   13x 13x 13x         2x 2x 2x   2x                                                                           11x                                                                                         1x                                                                               2x 2x 2x                                                                                                                                                                         14x 14x   14x 28x 14x                                   1x 1x   1x 1x         1x 2x 2x 2x 2x                                    
import SHARED from './shared';
import {store} from './store';
import objToStableString from 'fast-json-stable-stringify';
 
/**************************************************************
* Compute which platform we're on
*/
const userAgent = navigator.userAgent;
const platform = navigator.platform;
const edge = /Edge\/(\d+)/.exec(userAgent);
const ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
export const mac = ios || /Mac/.test(platform);
 
/**************************************************************
* Utility functions used in one or more files
*/
 
// make sure we never assign the same ID to two nodes in ANY active
// program at ANY point in time.
store.nodeCounter = 0;
export function gensym() {
  return (store.nodeCounter++).toString(16);
}
export function resetNodeCounter() { store.nodeCounter = 0; }
 
// Use reliable object->string library to generate a pseudohash,
// then hash the string so we don't have giant "hashes" eating memory
// (see https://stackoverflow.com/a/7616484/12026982 and
// https://anchortagdev.com/consistent-object-hashing-using-stable-stringification/ )
export function hashObject(obj) {
  const str = objToStableString(obj);
  var hash = 0, i, chr;
  Iif (str.length === 0) return hash;
  for (i = 0; i < str.length; i++) {
    chr   = str.charCodeAt(i);
    hash  = ((hash << 5) - hash) + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash;
};
 
// give (a,b), produce -1 if a<b, +1 if a>b, and 0 if a=b
export function poscmp(a, b) {
  Iif (!a) { console.log('utils:16, hitting null a'); }
  Iif (!b) { console.log('utils:16, hitting null b'); }
  return  a.line - b.line || a.ch - b.ch;
}
 
export function minpos(a, b) {
  return poscmp(a, b) <= 0 ? a : b;
}
 
export function maxpos(a, b) {
  return poscmp(a, b) >= 0 ? a : b;
}
 
// srcRangeIncludes(
//   outerRange: {from: Pos, to: Pos},
//   innerRange: {from: Pos, to: Pos})
// -> boolean
//
// Returns true iff innerRange is contained within outerRange.
export function srcRangeIncludes(outerRange, innerRange) {
  return poscmp(outerRange.from, innerRange.from) <= 0
    && poscmp(innerRange.to, outerRange.to) <= 0;
}
 
// srcRangeContains(range: {from: Pos, to: Pos}, pos: Pos) -> boolean
//
// Returns true iff `pos` is inside of `range`.
// (Being on the boundary counts as inside.)
export function srcRangeContains(range, pos) {
  return poscmp(range.from, pos) <= 0 && poscmp(pos, range.to);
}
 
export function skipWhile(skipper, start, next) {
  let now = start;
  while (skipper(now)) {
    now = next(now);
  }
  return now;
}
 
export function assert(x) {
  Iif (!x) {
    throw new Error("assertion fails");
  }
}
 
export function warn(origin, message) {
  console.warn(`CodeMirrorBlocks - ${origin} - ${message}`);
}
 
export function partition(arr, f) {
  const matched = [];
  const notMatched = [];
  for (const e of arr) {
    (f(e)? matched : notMatched).push(e);
  }
  return [matched, notMatched];
}
 
// // from https://davidwalsh.name/javascript-debounce-function
// export function debounce(func, wait, immediate) {
//   var timeout;
//   return function() {
//     var context = this, args = arguments;
//     var later = function() {
//       timeout = null;
//       if (!immediate) func.apply(context, args);
//     };
//     var callNow = immediate && !timeout;
//     clearTimeout(timeout);
//     timeout = setTimeout(later, wait);
//     if (callNow) func.apply(context, args);
//   };
// }
 
store.muteAnnouncements = false;
store.queuedAnnouncement = false;
 
// Note: screenreaders will automatically speak items with aria-labels!
// This handles _everything_else_.
export function say(text, delay=200, allowOverride=false) {
  const announcement = document.createTextNode(text + ', ');
  const announcer = SHARED.announcer;
  Iif (store.muteAnnouncements || !announcer) return; // if nothing to do, bail
  clearTimeout(store.queuedAnnouncement);            // clear anything overrideable
  if(allowOverride) {                                // enqueue overrideable announcements
    store.queuedAnnouncement = setTimeout(() => say('Use enter to edit', 0), delay);
  } else {                                           // otherwise write it to the DOM,
    announcer.childNodes.forEach( c => c.remove() ); // remove the children
    console.log('say:', text);                       // then erase it 10ms later
    setTimeout(() => announcer.appendChild(announcement), delay);
  }
}
 
export function createAnnouncement(nodes, action) {
  nodes.sort((a,b) => poscmp(a.from, b.from)); // speak first-to-last
  let annt = (action + " " +
    nodes.map((node) => node.options['aria-label'])
      .join(" and "));
  return annt;
}
 
export function skipCollapsed(node, next, state) {
  const {collapsedList, ast} = state;
  const collapsedNodeList = collapsedList.map(ast.getNodeById);
 
  // NOTE(Oak): if this is too slow, consider adding a
  // next/prevSibling attribute to short circuit navigation
  return skipWhile(
    node => node && collapsedNodeList.some(
      collapsed => ast.isAncestor(collapsed.id, node.id)
    ),
    next(node),
    next
  );
}
 
export function getRoot(node) {
  let next = node;
  // keep going until there's no next parent
  while (next && next.parent) { next = next.parent; }
  return next;
}
 
export function getLastVisibleNode(state) {
  const {collapsedList, ast} = state;
  const collapsedNodeList = collapsedList.map(ast.getNodeById);
  const lastNode = ast.getNodeBeforeCur(ast.reverseRootNodes[0].to);
  return skipWhile(
    node => !!node && node.parent && collapsedNodeList.some(
      collapsed => collapsed.id === node.parent.id),
    lastNode,
    n => n.parent
  );
}
 
export function withDefaults(obj, def) {
  return {...def, ...obj};
}
 
export function getBeginCursor(cm) {
  return CodeMirror.Pos(0, 0);
}
 
export function getEndCursor(cm) {
  return CodeMirror.Pos(
    cm.lastLine(),
    cm.getLine(cm.lastLine()).length
  );
}
 
export function posWithinNode(pos, node) {
  return (poscmp(node.from, pos) <= 0) && (poscmp(node.to, pos) >  0)
    ||   (poscmp(node.from, pos) <  0) && (poscmp(node.to, pos) >= 0);
}
 
function posWithinNodeBiased(pos, node) {
  return (poscmp(node.from, pos) <= 0) && (poscmp(node.to, pos) > 0);
}
 
export function nodeCommentContaining(pos, node) {
  return node.options.comment && posWithinNode(pos, node.options.comment);
}
 
export function getNodeContainingBiased(cursor, ast) {
  function iter(nodes) {
    const node = nodes.find(node => posWithinNodeBiased(cursor, node) || nodeCommentContaining(cursor, node));
    if (node) {
      const children = [...node.children()];
      if (children.length === 0) {
        return node;
      } else {
        const result = iter(children);
        return result === null ? node : result;
      }
    } else {
      return null;
    }
  }
  return iter(ast.rootNodes);
}
 
export const dummyPos = {line: -1, ch: 0};
 
export function isDummyPos(pos) {
  return pos.line === -1 && pos.ch === 0;
}
/*
// Announce, for testing purposes, that something important is about to update
// (like the DOM). Make sure to call `ready` after.
export function notReady(element) {
  SHARED.notReady[element] = null;
}
 
// Announce, for testing purposes, that an update previously registered with
// `notReady` has completed.
export function ready(element) {
  let thunk = SHARED.notReady[element];
  if (thunk) thunk();
  delete SHARED.notReady[element];
}
 
// For testing purposes, wait until everything (at least everything that knows
// to register itself with the `notReady` function) is ready. This should, e.g.,
// wait for DOM updates. DO NOT CALL THIS TWICE CONCURRENTLY, or it will not return.
export function waitUntilReady() {
  let waitingOn = Object.keys(SHARED.notReady).length;
  return new Promise(function(resolve, _) {
    for (let element of SHARED.notReady) {
      SHARED.notReady[element] = () => {
        waitingOn--;
        if (waitingOn === 0) {
          resolve();
        }
      };
    }
  });
}
*/
// Compute the position of the end of a change (its 'to' property refers to the pre-change end).
// based on https://github.com/codemirror/CodeMirror/blob/master/src/model/change_measurement.js
export function changeEnd({from, to, text}) {
  Iif (!text) return to;
  let lastLine = text[text.length - 1];
  return {
    line: from.line + text.length - 1,
    ch: lastLine.length + (text.length == 1 ? from.ch : 0)
  };
}
 
// Adjust a Pos to refer to the post-change position, or the end of the change if the change covers it.
// based on https://github.com/codemirror/CodeMirror/blob/master/src/model/change_measurement.js
export function adjustForChange(pos, change, from) {
  if (poscmp(pos, change.from) < 0)           return pos;
  if (poscmp(pos, change.from) == 0 && from)  return pos; // if node.from==change.from, no change
  if (poscmp(pos, change.to) <= 0)            return changeEnd(change);
  let line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch;
  if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch;
  return {line: line, ch: ch};
}
 
// Minimize a CodeMirror-style change object, by excluding any shared prefix
// between the old and new text. Mutates part of the change object.
export function minimizeChange({from, to, text, removed, origin=undefined}) {
  if (!removed) removed = SHARED.cm.getRange(from, to).split("\n");
  // Remove shared lines
  while (text.length >= 2 && text[0] && removed[0] && text[0] === removed[0]) {
    text.shift();
    removed.shift();
    from.line += 1;
    from.ch = 0;
  }
  // Remove shared chars
  let n = 0;
  for (let i in text[0]) {
    if (text[0][i] !== removed[0][i]) break;
    n = (+i) + 1;
  }
  text[0] = text[0].substr(n);
  removed[0] = removed[0].substr(n);
  from.ch += n;
  // Return the result.
  return origin ? {from, to, text, removed, origin} : {from, to, text, removed};
}
 
// display the actual exception, and try to log it
export function logResults(history, exception, description="Crash Log") {
  console.log(exception, history);
  try {
    document.getElementById('description').value = description;
    document.getElementById('history').value = JSON.stringify(history);
    document.getElementById('exception').value = exception;
    document.getElementById('errorLogForm').submit();
  } catch (e) {
    console.log('LOGGING FAILED.', e, history);
  }
}
 
export function validateRanges(ranges, ast) {
  ranges.forEach(({anchor, head}) => {
    const c1 = minpos(anchor, head);
    const c2 = maxpos(anchor, head);
    if(ast.getNodeAt(c1, c2)) return;  // if there's a node, it's a valid range
    // Top-Level if there's no node, or it's a root node with the cursor at .from or .to
    const N1 = ast.getNodeContaining(c1); // get node containing c1
    const N2 = ast.getNodeContaining(c2); // get node containing c2
    const c1IsTopLevel = !N1 || (!N1.parent && (!poscmp(c1, N1.from) || !poscmp(c1, N1.to)));
    const c2IsTopLevel = !N2 || (!N2.parent && (!poscmp(c2, N2.from) || !poscmp(c2, N2.to)));
 
    // If they're both top-level, it's a valid text range
    if(c1IsTopLevel && c2IsTopLevel) return;
 
    // Otherwise, the range is neither toplevel OR falls neatly on a node boundary
    throw new `The range {line:${c1.line}, ch:${c1.ch}}, {line:${c2.line}, 
      ch:${c2.ch}} partially covers a node, which is not allowed`;
  });
  return true;
}
 
 
export class BlockError extends Error {
  constructor(message, type, data) {
    super(message);
    this.type = type;
    this.data = data;
  }
}
 
export function topmostUndoable(which, state) {
  Eif (!state) state = store.getState();
  let arr = (which === 'undo' ?
    SHARED.cm.doc.history.done : SHARED.cm.doc.history.undone);
  for (let i = arr.length - 1; i >= 0; i--) {
    if (!arr[i].ranges) {
      return arr[i];
    }
  }
}
 
export function preambleUndoRedo(which) {
  let state = store.getState();
  let tU = topmostUndoable(which);
  if (tU) {
    say((which === 'undo' ? 'UNDID' : 'REDID') + ': ' + tU.undoableAction);
    state.undoableAction = tU.undoableAction;
    state.actionFocus = tU.actionFocus;
  }
}
 
/****************************************************************
* SOUND HANDLING
*/
import beepSound from './ui/beep.mp3';
export const BEEP = new Audio(beepSound);
 
import wrapSound from './ui/wrap.mp3';
export const WRAP = new Audio(wrapSound);
 
// for each sound resource, set crossorigin value to "anonymous"
// and set up state for interruptable playback 
// (see https://stackoverflow.com/a/40370077/12026982)
[BEEP, WRAP].forEach(sound => {
  sound.crossorigin = "anonymous";
  sound.isPlaying = false;
  sound.onplaying = function(){ this.isPlaying = true;  };
  sound.onpause   = function(){ this.isPlaying = false; };
});
 
export function playSound(sound) {
  sound.pause();
  console.log("BEEP!");
  if (!(sound.paused && !sound.isPlaying)) return;
  if (sound.readyState > 0) sound.currentTime = 0;
  // Promise handling from: https://goo.gl/xX8pDD
  // In browsers that don’t yet support this functionality,
  // playPromise won’t be defined.
  var playPromise = sound.play();
  if (playPromise !== undefined) {
    playPromise
      .then(  () => {}) // Automatic playback started!
      .catch( () => {});// Automatic playback failed.
  }
}