text: /**
  * Class for a crossword grid
  **/
  class xwGrid {
      constructor(cells) {
          this.cells = cells;
          this.height = Math.max.apply(null, cells.map(c => parseInt(c.y))) + 1;
          this.width = Math.max.apply(null, cells.map(c => parseInt(c.x))) + 1;
          this.numbers = this.gridNumbering();
      }
      /* return the cell at (x,y) */
      cellAt(x, y) {
          return this.cells.find((cell) => (cell.x == x && cell.y == y));
      }
      letterAt(x, y) {
          return this.cellAt(x, y).solution;
      }
      isBlack(x, y) {
          var thisCell = this.cellAt(x, y);
          return (thisCell.type == 'void' || thisCell.type == 'block');
      }
      /* check if we have a black square in a given direction */
      hasBlack(x, y, dir) {
          var mapping_dict = {
            'right': {'xcheck': this.width-1, 'xoffset': 1, 'yoffset': 0, 'dir2': 'left'}
          , 'left': {'xcheck': 0, 'xoffset': -1, 'yoffset': 0, 'dir2': 'right'}
          , 'top': {'ycheck': 0, 'xoffset': 0, 'yoffset': -1, 'dir2': 'bottom'}
          , 'bottom': {'ycheck': this.height-1, 'xoffset': 0, 'yoffset': 1, 'dir2': 'top'}
          };
          var md = mapping_dict[dir];
          if (x === md['xcheck'] || y === md['ycheck']) {
            return true;
          }
          else if (this.isBlack(x + md['xoffset'], y + md['yoffset'])) {
            return true;
          }
          else if (this.cellAt(x, y)[dir + '-bar']) {
            return true;
          }
          else if (this.cellAt(x + md['xoffset'], y + md['yoffset'])[md['dir2'] + '-bar']) {
            return true;
          }
          return false;
      }
  
      // both startAcrossWord and startDownWord have to account for bars
      startAcrossWord(x, y) {
          return this.hasBlack(x, y, 'left') && x < this.width - 1 && !this.isBlack(x, y) && !this.hasBlack(x, y, 'right');
      }
      startDownWord(x, y) {
          return this.hasBlack(x, y, 'top') && y < this.height - 1 && !this.isBlack(x, y) && !this.hasBlack(x, y, 'bottom');
      }
      // An array of grid numbers
      gridNumbering() {
          var numbers = [];
          var thisNumber = 1;
          for (var y=0; y < this.height; y++) {
              var thisNumbers = [];
              for (var x=0; x < this.width; x++) {
                  if (this.startAcrossWord(x, y) || this.startDownWord(x, y)) {
                      thisNumbers.push(thisNumber);
                      thisNumber += 1;
                  }
                  else {
                      thisNumbers.push(0);
                  }
              }
              numbers.push(thisNumbers);
          }
          return numbers;
      }
  
      acrossEntries() {
          var acrossEntries = {}, x, y, thisNum;
          for (y = 0; y < this.height; y++) {
              for (x = 0; x < this.width; x++) {
                  if (this.startAcrossWord(x, y)) {
                      thisNum = this.numbers[y][x];
                      if (!acrossEntries[thisNum] && thisNum) {
                          acrossEntries[thisNum] = {'word': '', 'cells': []};
                      }
                  }
                  if (!this.isBlack(x, y) && thisNum) {
                      var letter = this.letterAt(x, y);
                      acrossEntries[thisNum]['word'] += letter;
                      acrossEntries[thisNum]['cells'].push([x, y]);
                  }
                  // end the across entry if we hit the edge
                  if (this.hasBlack(x, y, 'right')) {
                      thisNum = null;
                  }
              }
          }
          return acrossEntries;
      }
  
      downEntries() {
          var downEntries = {}, x, y, thisNum;
          for (x = 0; x < this.width; x++) {
              for (y = 0; y < this.height; y++) {
                  if (this.startDownWord(x, y)) {
                      thisNum = this.numbers[y][x];
                      if (!downEntries[thisNum] && thisNum) {
                          downEntries[thisNum] = {'word': '', 'cells': []};
                      }
                  }
                  if (!this.isBlack(x, y) && thisNum) {
                      var letter = this.letterAt(x, y);
                      downEntries[thisNum]['word'] += letter;
                      downEntries[thisNum]['cells'].push([x, y]);
                  }
                  // end the down entry if we hit the bottom
                  if (this.hasBlack(x, y, 'bottom')) {
                      thisNum = null;
                  }
              }
          }
          return downEntries;
      }
  }
  
  /**
  * Since we're reading everything in as a binary string
  * we need a function to convert to a UTF-8 string
  * Note that if .readAsBinaryString() goes away,
  * we will have to change both this and the reading method.
  **/
  function BinaryStringToUTF8String(x) {
      // convert to bytes array
      var bytes = [];
      for (var i = 0; i < x.length; ++i) {
        var code = x.charCodeAt(i);
        bytes.push([code]);
      }
      var bytes1 = new Uint8Array(bytes);
      return new TextDecoder("utf-8").decode(bytes1);
  }
  
  // function to get an index from an i, j, and width
  function xw_ij_to_index(i, j, w) {
      return j * w + i;
  }
  
  // function to convert an index to i, j
  function xw_index_to_ij(ix, w) {
      var j = Math.floor(ix/w);
      var i = ix - j*w;
      return [i, j];
  }
  
  /** Generic file download function **/
  function file_download(data, filename, type) {
      var file = new Blob([data], {type: type});
      if (window.navigator.msSaveOrOpenBlob) // IE10+
          window.navigator.msSaveOrOpenBlob(file, filename);
      else { // Others
          var a = document.createElement("a"),
                  url = URL.createObjectURL(file);
          a.href = url;
          a.download = filename;
          document.body.appendChild(a);
          a.click();
          setTimeout(function() {
              document.body.removeChild(a);
              window.URL.revokeObjectURL(url);
          }, 0);
      }
  }
  
  class JSCrossword {
      /*
      * `metadata` has
        - title, author, copyright, description (notes)
        - height, width
        - crossword_type (crossword, coded, acrostic)
        OPTIONAL:
        - has_reveal (default: true)
        - completion_message
      * `cells` is an array of cells with the various attributes
        - x and y (0-indexed)
        - "type" = 'block' if it's a block
        - "number" = number if it's numbered
        - "solution" = letter(s) that go in the box
        - others: background-color (RGB), background-shape (circle),
            bottom-bar, right-bar, top-bar, left-bar (= true if exist)
            top_right_number
            value (a filled-in letter, if any)
  
      * `words` is an array of objects, each with an "id" and a "cells" attribute
        "id" is just a unique number to match up with the clues.
        "cells" is an array of objects giving the x and y values, in order
      * `clues` is an array of (usually) two objects.
         each object within has a "title" key whose value is generally "ACROSS" or "DOWN"
         and a "clue" key, whose value is an array of clues.
         Each "clue" key has
           - a "text" value which is the actual clue
           - a "word" which is the associated word ID
           - an optional "number"
      */
      constructor(metadata, cells, words, clues) {
          this.metadata = metadata;
          this.cells = cells;
          this.words = words;
          this.clues = clues;
      }
  
      //CROSSWORD_TYPES = ['crossword', 'coded', 'acrostic'];
  
      /**
      * useful functions
      **/
  
      /** set has_check and has_reveal as needed **/
      set_check_reveal() {
          var all_letters = new Set(this.cells.map(x=> x.solution));
          if (all_letters.size <= 2) {
              this.metadata.has_check = false;
              this.metadata.has_reveal = false;
          }
      }
  
      /** Create a solution array **/
      create_solution_array() {
          // initialize an empty array given the width and height
          var h = this.metadata.height;
          var w = this.metadata.width;
          var solutionArray = Array.from({ length: h }, () =>
              Array.from({ length: w }, () => false)
          );
          // Loop through the "cells" array and populate
          this.cells.forEach(function(c) {
              solutionArray[c.y][c.x] = c.solution;
          });
          this.solution_array = solutionArray;
      }
  
      /** Get the solution array **/
      get_solution_array() {
          if (!this.solution_array) {
              this.create_solution_array();
          }
          return this.solution_array;
      }
  
      /** Create a mapping of word ID to entry **/
      create_entry_mapping() {
          var soln_arr = this.get_solution_array();
          var entryMapping = {};
          this.words.forEach(function(w) {
              var _id = w.id;
              var entry = '';
              w.cells.forEach(function(arr) {
                  entry += soln_arr[arr[1]][arr[0]];
              });
              entryMapping[_id] = entry;
          });
          this.entry_mapping = entryMapping;
      }
  
      /** Get the entry mapping **/
      get_entry_mapping() {
          if (!this.entry_mapping) {
              this.create_entry_mapping();
          }
          return this.entry_mapping;
      }
  
      /**
      * Read in data from various file formats
      **/
  
      /** puz **/
      // requires puz_read_write.js
      fromPuz(contents) {
          // Read data into a puzapp
          var puzdata = PUZAPP.parsepuz(contents);
          return jscrossword_from_puz(puzdata);
      }
  
      /** JPZ **/
      // requires jpz_read_write.js (and JSZip??)
      fromJPZ(data) {
          return xw_read_jpz(data);
      }
  
      /** iPUZ **/
      // requires ipuz_read_write.js
      fromIpuz(data) {
          return xw_read_ipuz(data);
      }
  
      /* try to determine the puzzle type */
      fromData(data) {
          var js;
          try {
              var puzdata = PUZAPP.parsepuz(data);
              js = jscrossword_from_puz(puzdata);
          } catch (error) {
              console.log(error);
              try {
                  js = xw_read_jpz(data);
              } catch (error2) {
                  console.log(error2);
                  js = xw_read_ipuz(data);
              }
          }
          js.set_check_reveal();
          return js;
      }
  
      /**
      * Write data for downloads
      **/
  
      /**
      * write JPZ
      **/
      toJPZString() {
          return xw_write_jpz(this.metadata, this.cells, this.words, this.clues);
      }
  }
  
  /**
  * Secure Hash Algorithm (SHA1)
  * http://www.webtoolkit.info/
  **/
  // This is helpful for checking if IPUZ-locked files are correctly solved
  function SHA1(r){function o(r,o){return r<<o|r>>>32-o}function e(r){var o,e="";for(o=7;o>=0;o--)e+=(r>>>4*o&15).toString(16);return e}var t,a,h,n,C,c,f,d,A,u=new Array(80),g=1732584193,i=4023233417,s=2562383102,S=271733878,m=3285377520,p=(r=function(r){r=r.replace(/\r\n/g,"\n");for(var o="",e=0;e<r.length;e++){var t=r.charCodeAt(e);t<128?o+=String.fromCharCode(t):t>127&&t<2048?(o+=String.fromCharCode(t>>6|192),o+=String.fromCharCode(63&t|128)):(o+=String.fromCharCode(t>>12|224),o+=String.fromCharCode(t>>6&63|128),o+=String.fromCharCode(63&t|128))}return o}(r)).length,l=new Array;for(a=0;a<p-3;a+=4)h=r.charCodeAt(a)<<24|r.charCodeAt(a+1)<<16|r.charCodeAt(a+2)<<8|r.charCodeAt(a+3),l.push(h);switch(p%4){case 0:a=2147483648;break;case 1:a=r.charCodeAt(p-1)<<24|8388608;break;case 2:a=r.charCodeAt(p-2)<<24|r.charCodeAt(p-1)<<16|32768;break;case 3:a=r.charCodeAt(p-3)<<24|r.charCodeAt(p-2)<<16|r.charCodeAt(p-1)<<8|128}for(l.push(a);l.length%16!=14;)l.push(0);for(l.push(p>>>29),l.push(p<<3&4294967295),t=0;t<l.length;t+=16){for(a=0;a<16;a++)u[a]=l[t+a];for(a=16;a<=79;a++)u[a]=o(u[a-3]^u[a-8]^u[a-14]^u[a-16],1);for(n=g,C=i,c=s,f=S,d=m,a=0;a<=19;a++)A=o(n,5)+(C&c|~C&f)+d+u[a]+1518500249&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=20;a<=39;a++)A=o(n,5)+(C^c^f)+d+u[a]+1859775393&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=40;a<=59;a++)A=o(n,5)+(C&c|C&f|c&f)+d+u[a]+2400959708&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=60;a<=79;a++)A=o(n,5)+(C^c^f)+d+u[a]+3395469782&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;g=g+n&4294967295,i=i+C&4294967295,s=s+c&4294967295,S=S+f&4294967295,m=m+d&4294967295}return(A=e(g)+e(i)+e(s)+e(S)+e(m)).toLowerCase()}
  