smartdown-gallery
Version:
Example Smartdown documents and associated resources that demonstrate various Smartdown features and serve as raw material for other Smartdown demos.
1,576 lines (1,489 loc) • 102 kB
JavaScript
'use strict';
/*
MIT License
Copyright (c) 2019 Viresh Ratnakar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The latest code and documentation for exolve can be found at:
https://github.com/viresh-ratnakar/exolve
*/
const VERSION = 'Exolve v0.36 October 22 2019 (w/reentrancy via DoctorBud)'
let puzzleInstanceId = 0;
let puzzleInstances = {};
const exolvePrefix = 'exolve-';
class Puzzle {
constructor() {
const puzzleThis = this;
puzzleInstanceId = puzzleInstanceId + 1;
this.id = puzzleInstanceId;
const puzzlePrefix = `${exolvePrefix}${puzzleInstanceId}-`;
puzzleInstances[puzzlePrefix] = this;
// ------ Begin globals.
let puzzleId = 'exolve-grid'
let gridWidth = 0
let gridHeight = 0
let boxWidth = 0
let boxHeight = 0
let gridFirstLine = -1
let gridLastLine = -1
let preludeFirstLine = -1
let preludeLastLine = -1
let acrossFirstLine = -1
let acrossLastLine = -1
let downFirstLine = -1
let downLastLine = -1
let nodirFirstLine = -1
let nodirLastLine = -1
let explanationsFirstLine = -1
let explanationsLastLine = -1
// Each nina will be an array containing location [i,j] pairs and/or span
// class names.
let ninas = []
// For span-class-specified ninas, ninaClassElements[] stores the elements
// along with the colours to apply to them when showing the ninas.
let ninaClassElements = []
let showingNinas = false
let grid = []
let clues = {}
let cellColours = []
let submitURL = null
let submitKeys = []
let hasDiagramlessCells = false
let hasUnsolvedCells = false
let hasSomeAnnos = false
let hasAcrossClues = false
let hasDownClues = false
let hasNodirClues = false
// Clues labeled non-numerically (like [A] a clue...) use this to create a
// unique clueIndex.
let nextNonNumId = 1
let nonNumClueIndices = {}
const SQUARE_DIM = 31
const SQUARE_DIM_BY2 = 16
const GRIDLINE = 1
const BAR_WIDTH = 3
const BAR_WIDTH_BY2 = 2
const SEP_WIDTH = 2
const SEP_WIDTH_BY2 = 1.5
const HYPHEN_WIDTH = 9
const HYPHEN_WIDTH_BY2 = 5
const CIRCLE_RADIUS = 0.0 + SQUARE_DIM / 2.0
const NUMBER_START_X = 2
const NUMBER_START_Y = 10
const LIGHT_START_X = 16.5
const LIGHT_START_Y = 21.925
let answersList = []
let revelationList = []
let currentRow = -1
let currentCol = -1
let currentDirectionIsAcross = true
let currentClueIndex = null
let activeCells = [];
let activeClues = [];
let numCellsToFill = 0
let allClueIndices = []
let orphanClueIndices = []
// For the orhpan-clues widget.
let posInOrphanClueIndices = 0
const BLOCK_CHAR = '⬛';
const ACTIVE_COLOUR = 'mistyrose'
const ORPHAN_CLUES_COLOUR = 'white'
const TRANSPARENT_WHITE = 'rgba(255,255,255,0.0)'
let nextPuzzleTextLine = 0
const STATE_SEP = 'eexxoollvvee'
// Variables set by exolve-option
let hideInferredNumbers = false
let cluesPanelLines = -1
// Variables set in init().
let puzzleTextLines;
let numPuzzleTextLines;
let svg;
let gridInputWrapper;
let gridInput;
let questions;
let background;
let acrossClues;
let downClues;
let nodirClues;
let acrossPanel;
let downPanel;
let nodirPanel;
let currentClue;
let currentClueParent;
let ninaGroup;
let statusNumFilled;
let statusNumTotal;
let savingURL;
let clearButton;
let clearAllButton;
let checkButton;
let checkAllButton;
let ninasButton;
let revealButton;
let revealAllButton;
let submitButton;
// ------ End globals.
// ------ Begin functions.
// Set up globals, version number and user agent in bug link.
function init(puzzleText) {
puzzleTextLines = []
let rawLines = puzzleText.trim().split('\n');
for (let rawLine of rawLines) {
let cIndex = rawLine.indexOf('#');
// A # followed by a non-space/non-eol character is not a comment marker.
while (cIndex >= 0 && cIndex + 1 < rawLine.length &&
rawLine.charAt(cIndex + 1) != ' ') {
cIndex = rawLine.indexOf('#', cIndex + 1);
}
if (cIndex >= 0) {
rawLine = rawLine.substr(0, cIndex).trim()
}
if (!rawLine) {
continue;
}
puzzleTextLines.push(rawLine)
}
numPuzzleTextLines = puzzleTextLines.length
svg = document.getElementById(puzzlePrefix + 'grid');
gridInputWrapper = document.getElementById(puzzlePrefix + 'grid-input-wrapper');
gridInput = document.getElementById(puzzlePrefix + 'grid-input');
questions = document.getElementById(puzzlePrefix + 'questions');
background =
document.createElementNS('http://www.w3.org/2000/svg', 'rect');
acrossPanel = document.getElementById(puzzlePrefix + 'across-clues-panel')
downPanel = document.getElementById(puzzlePrefix + 'down-clues-panel')
nodirPanel = document.getElementById(puzzlePrefix + 'nodir-clues-panel')
acrossClues = document.getElementById(puzzlePrefix + 'across')
downClues = document.getElementById(puzzlePrefix + 'down')
nodirClues = document.getElementById(puzzlePrefix + 'nodir')
currentClue = document.getElementById(puzzlePrefix + 'current-clue')
currentClueParent = document.getElementById(puzzlePrefix + 'current-clue-parent')
ninaGroup = document.getElementById(puzzlePrefix + 'nina-group')
statusNumFilled = document.getElementById(puzzlePrefix + 'status-num-filled')
statusNumTotal = document.getElementById(puzzlePrefix + 'status-num-total')
savingURL = document.getElementById(puzzlePrefix + 'saving-url')
clearButton = document.getElementById(puzzlePrefix + 'clear')
clearAllButton = document.getElementById(puzzlePrefix + 'clear-all')
checkButton = document.getElementById(puzzlePrefix + 'check')
checkAllButton = document.getElementById(puzzlePrefix + 'check-all')
ninasButton = document.getElementById(puzzlePrefix + 'ninas')
revealButton = document.getElementById(puzzlePrefix + 'reveal')
revealAllButton = document.getElementById(puzzlePrefix + 'reveal-all')
submitButton = document.getElementById(puzzlePrefix + 'submit')
let info = 'Version: ' + VERSION + ', User Agent: ' + navigator.userAgent
document.getElementById(puzzlePrefix + 'report-bug').href =
'https://github.com/viresh-ratnakar/exolve/issues/new?body=' +
encodeURIComponent(info);
}
// puzzleTextLines[] has been parsed till line # nextPuzzleTextLine. Fine the
// next line beginning with 'exolve-<section>' and return <section> as well
// as the 'value' of the section (the part after ':').
function parseToNextSection() {
const MARKER = 'exolve-'
while (nextPuzzleTextLine < numPuzzleTextLines &&
puzzleTextLines[nextPuzzleTextLine].trim().indexOf(MARKER) != 0) {
nextPuzzleTextLine++;
}
if (nextPuzzleTextLine >= numPuzzleTextLines) {
return null
}
// Skip past MARKER
let line = puzzleTextLines[nextPuzzleTextLine].trim().substr(MARKER.length)
let index = line.indexOf(':')
if (index < 0) {
index = line.length
}
nextPuzzleTextLine++
return {'section': line.substr(0, index).trim().toLowerCase(),
'value': line.substr(index + 1).trim()}
}
// Parse a nina line, which consists of cell locations of the nina specified
// using "chess notation" (a1 = bottom-left, etc.). Convert the cell locations
// to [row col] and push an array of these locations to the global ninas array.
function parseNina(s) {
let nina = []
let cellsOrClasses = s.split(' ')
for (let cellOrClass of cellsOrClasses) {
let cellLocation = parseCellLocation(cellOrClass)
if (!cellLocation) {
// Must be a class name, for a span-class-specified nina
nina.push(cellOrClass)
} else {
nina.push(cellLocation)
}
}
if (nina.length > 0) {
ninas.push(nina)
}
}
function parseColour(s) {
let colourAndCells = s.split(' ')
let colour = ''
for (let c of colourAndCells) {
if (!colour) {
colour = c
continue;
}
let cellLocation = parseCellLocation(c)
if (!cellLocation) {
addError('Could not parse cell location in: ' + c)
return
} else {
cellColours.push(cellLocation.concat(colour))
}
}
}
// Parse a question line and create the question element for it (which includes
// an input box for the answer). The solution answer may be provided after the
// last ')'.
function parseQuestion(s) {
let enumParse = parseEnum(s)
let inputLen = enumParse.len + enumParse.hyphenAfter.length +
enumParse.wordEndAfter.length
let afterEnum = enumParse.afterEnum
let rawQ = s.substr(0, afterEnum)
let hideEnum = false
if (inputLen > 0) {
if (s.substr(afterEnum, 1) == '*') {
beforeEnum = s.lastIndexOf('(', afterEnum - 1)
if (beforeEnum < 0) {
addError('Could not find open-paren strangely')
return
}
rawQ = s.substr(0, beforeEnum)
afterEnum++
hideEnum = true
}
}
let correctAnswer = s.substr(afterEnum).trim()
const question = document.createElement('div')
question.setAttributeNS(null, 'class', 'question');
const questionText = document.createElement('span')
questionText.innerHTML = rawQ
question.appendChild(questionText)
question.appendChild(document.createElement('br'))
if (inputLen == 0) {
hideEnum = true
inputLen = '30'
}
const TEXTAREA_COLS = 68
let rows = Math.floor(inputLen / TEXTAREA_COLS)
if (rows * TEXTAREA_COLS < inputLen) {
rows++
}
let cols = (rows > 1) ? TEXTAREA_COLS : inputLen
let aType = 'input'
if (rows > 1) {
aType = 'textarea'
}
const answer = document.createElement(aType)
if (rows > 1) {
answer.setAttributeNS(null, 'rows', '' + rows);
answer.setAttributeNS(null, 'cols', '' + cols);
} else {
answer.setAttributeNS(null, 'size', '' + cols);
}
answer.setAttributeNS(null, 'class', 'answer');
answersList.push({
'ans': correctAnswer,
'input': answer,
'hasEnum': (inputLen > 0),
});
if (!hideEnum) {
let answerValue = ''
let wordEndIndex = 0
let hyphenIndex = 0
for (let i = 0; i < enumParse.len; i++) {
answerValue = answerValue + '?'
if (wordEndIndex < enumParse.wordEndAfter.length &&
i == enumParse.wordEndAfter[wordEndIndex]) {
answerValue = answerValue + ' '
wordEndIndex++
}
if (hyphenIndex < enumParse.hyphenAfter.length &&
i == enumParse.hyphenAfter[hyphenIndex]) {
answerValue = answerValue + '-'
hyphenIndex++
}
}
answer.setAttributeNS(null, 'placeholder', '' + answerValue);
}
answer.setAttributeNS(null, 'class', 'answer');
if (rows == 1) {
answer.setAttributeNS(null, 'type', 'text');
}
answer.setAttributeNS(null, 'maxlength', '' + inputLen);
answer.setAttributeNS(null, 'autocomplete', 'off');
answer.setAttributeNS(null, 'spellcheck', 'false');
question.appendChild(answer)
questions.appendChild(question)
answer.addEventListener('input', updateAndSaveState);
}
function parseSubmit(s) {
let parts = s.split(' ')
if (s.length < 2) {
addError('Submit section must have a URL and a param name for the solution')
return
}
submitURL = parts[0]
submitKeys = []
for (let i = 1; i < parts.length; i++) {
submitKeys.push(parts[i])
}
}
function parseOption(s) {
let sparts = s.split(' ')
for (let spart of sparts) {
spart = spart.trim().toLowerCase()
if (spart == "hide-inferred-numbers") {
hideInferredNumbers = true
continue
}
let kv = spart.split(':')
if (kv.length != 2) {
addError('Expected exolve-option: key:value, got: ' + spart)
return
}
if (kv[0] == 'clues-panel-lines') {
cluesPanelLines = parseInt(kv[1])
if (isNaN(cluesPanelLines)) {
addError('Unexpected value in exolve-option: clue-panel-lines: ' + kv[1])
}
continue
}
addError('Unexpected exolve-option: ' + spart)
return
}
}
// The overall parser for the puzzle text. Also takes care of parsing and
// displaying all exolve-* sections except prelude, grid, across, down (for
// these, it just captures where the start and end lines are).
function parseOverallDisplayMost() {
let sectionAndValue = parseToNextSection()
while (sectionAndValue && sectionAndValue.section != 'end') {
let firstLine = nextPuzzleTextLine
let nextSectionAndValue = parseToNextSection()
let lastLine = nextPuzzleTextLine - 2
if (sectionAndValue.section == 'begin') {
} else if (sectionAndValue.section == 'id') {
puzzleId = sectionAndValue.value
} else if (sectionAndValue.section == 'title') {
document.getElementById(puzzlePrefix + 'title').innerHTML = sectionAndValue.value
} else if (sectionAndValue.section == 'setter') {
if (sectionAndValue.value.trim() != '') {
document.getElementById(puzzlePrefix + 'setter').innerHTML =
'By ' + sectionAndValue.value
}
} else if (sectionAndValue.section == 'copyright') {
document.getElementById(puzzlePrefix + 'copyright').innerHTML =
'Ⓒ ' + sectionAndValue.value
} else if (sectionAndValue.section == 'width') {
gridWidth = parseInt(sectionAndValue.value)
boxWidth = (SQUARE_DIM * gridWidth) + gridWidth + 1
} else if (sectionAndValue.section == 'height') {
gridHeight = parseInt(sectionAndValue.value)
boxHeight = (SQUARE_DIM * gridHeight) + gridHeight + 1
} else if (sectionAndValue.section == 'prelude') {
preludeFirstLine = firstLine
preludeLastLine = lastLine
} else if (sectionAndValue.section == 'grid') {
gridFirstLine = firstLine
gridLastLine = lastLine
} else if (sectionAndValue.section == 'nina') {
parseNina(sectionAndValue.value)
} else if (sectionAndValue.section == 'colour' ||
sectionAndValue.section == 'color') {
parseColour(sectionAndValue.value)
} else if (sectionAndValue.section == 'question') {
parseQuestion(sectionAndValue.value)
} else if (sectionAndValue.section == 'submit') {
parseSubmit(sectionAndValue.value)
} else if (sectionAndValue.section == 'across') {
acrossFirstLine = firstLine
acrossLastLine = lastLine
} else if (sectionAndValue.section == 'down') {
downFirstLine = firstLine
downLastLine = lastLine
} else if (sectionAndValue.section == 'nodir') {
nodirFirstLine = firstLine
nodirLastLine = lastLine
} else if (sectionAndValue.section == 'option') {
parseOption(sectionAndValue.value)
} else if (sectionAndValue.section == 'explanations') {
explanationsFirstLine = firstLine
explanationsLastLine = lastLine
}
sectionAndValue = nextSectionAndValue
}
}
// Extracts the prelude from its previously identified lines and sets up
// its display.
function parseAndDisplayPrelude() {
if (preludeFirstLine >= 0 && preludeFirstLine <= preludeLastLine) {
let preludeText = puzzleTextLines[preludeFirstLine]
let l = preludeFirstLine + 1
while (l <= preludeLastLine) {
preludeText = preludeText + '\n' + puzzleTextLines[l]
l++;
}
document.getElementById(puzzlePrefix + 'prelude').innerHTML = preludeText
}
}
// Extracts the explanations section from its previously identified lines,
// populates its element, and adds it to revelationList.
function parseAndDisplayExplanations() {
if (explanationsFirstLine >= 0 &&
explanationsFirstLine <= explanationsLastLine) {
let explanationsText = puzzleTextLines[explanationsFirstLine]
let l = explanationsFirstLine + 1
while (l <= explanationsLastLine) {
explanationsText = explanationsText + '\n' + puzzleTextLines[l]
l++;
}
const explanations = document.getElementById(puzzlePrefix + 'explanations')
explanations.innerHTML = explanationsText
revelationList.push(explanations)
}
}
// Append an error message to the errors div. Scuttle everything by seting
// gridWidth to 0.
function addError(error) {
document.getElementById(puzzlePrefix + 'errors').innerHTML =
document.getElementById(puzzlePrefix + 'errors').innerHTML + '<br/>' +
error;
gridWidth = 0
}
// Run some checks for serious problems with grid id, dimensions, etc. If found,
// abort with error.
function checkIdAndConsistency() {
if (puzzleId.match(/[^a-zA-Z\d-]/)) {
addError('Puzzle id should only have alphanumeric characters or -: ' +
puzzleId)
return
}
if (gridWidth < 1 || gridWidth > 25 || gridHeight < 1 || gridHeight > 25) {
addError('Bad/missing width/height');
return
} else if (gridFirstLine < 0 || gridLastLine < gridFirstLine ||
gridHeight != gridLastLine - gridFirstLine + 1) {
addError('Mismatched width/height');
return
}
for (let i = 0; i < gridHeight; i++) {
let lineW = puzzleTextLines[i + gridFirstLine].toUpperCase().
replace(/[^A-Z.0]/g, '').length
if (gridWidth != lineW) {
addError('Width in row ' + i + ' is ' + lineW + ', not ' + gridWidth);
return
}
}
if (submitURL && submitKeys.length != answersList.length + 1) {
addError('Have ' + submitKeys.length + ' submit paramater keys, need ' +
(answersList.length + 1));
return
}
}
// Parse grid lines into a gridWidth x gridHeight array of objects that have
// the following properties:
// isLight
// hasBarAfter
// hasBarUnder
// hasCircle
// isDiagramless
// startsClueLabel
// startsAcrossClue
// startsDownClue
// acrossClueLabel: #
// downClueLabel: #
// Also set the following globals:
// hasDiagramlessCells
// hasUnsolvedCells
function parseGrid() {
let hasSolvedCells = false
for (let i = 0; i < gridHeight; i++) {
grid[i] = new Array(gridWidth)
let gridLine = puzzleTextLines[i + gridFirstLine].
replace(/\s/g, '').toUpperCase()
let gridLineIndex = 0
for (let j = 0; j < gridWidth; j++) {
grid[i][j] = {};
let letter = gridLine.charAt(gridLineIndex);
if (letter != '.') {
grid[i][j].isLight = true
if (letter != '0') {
letter = letter.toUpperCase()
if (letter < 'A' || letter > 'Z') {
addError('Bad grid entry: ' + letter);
gridWidth = 0
return
}
grid[i][j].solution = letter
}
} else {
grid[i][j].isLight = false
}
grid[i][j].hasBarAfter = false
grid[i][j].hasBarUnder = false
grid[i][j].hasCircle = false
grid[i][j].isDiagramless = false
grid[i][j].prefill = false
gridLineIndex++
let thisChar = ''
while (gridLineIndex < gridLine.length &&
(thisChar = gridLine.charAt(gridLineIndex)) &&
(thisChar == '|' ||
thisChar == '_' ||
thisChar == '+' ||
thisChar == '@' ||
thisChar == '*' ||
thisChar == '!' ||
thisChar == ' ')) {
if (thisChar == '|') {
grid[i][j].hasBarAfter = true
} else if (thisChar == '_') {
grid[i][j].hasBarUnder = true
} else if (thisChar == '+') {
grid[i][j].hasBarAfter = true
grid[i][j].hasBarUnder = true
} else if (thisChar == '@') {
grid[i][j].hasCircle = true
} else if (thisChar == '*') {
grid[i][j].isDiagramless = true
} else if (thisChar == '!') {
grid[i][j].prefill = true
} else if (thisChar == ' ') {
} else {
addError('Should not happen! thisChar = ' + thisChar);
return
}
gridLineIndex++
}
if (grid[i][j].isDiagramless && letter == '.') {
grid[i][j].solution = '1'
}
if (grid[i][j].prefill &&
(!grid[i][j].isLight || letter < 'A' || letter > 'Z')) {
addError('Bad pre-filled cell (' + i + ',' + j +
') with letter: ' + letter)
return
}
if (grid[i][j].isDiagramless) {
hasDiagramlessCells = true
}
if (letter == '0') {
hasUnsolvedCells = true
}
if (letter >= 'A' && letter <= 'Z' && !grid[i][j].prefill) {
hasSolvedCells = true
}
}
}
if (hasUnsolvedCells && hasSolvedCells) {
addError('Either all or no solutions should be provided')
}
}
function startsAcrossClue(i, j) {
if (!grid[i][j].isLight) {
return false;
}
if (j > 0 && grid[i][j - 1].isLight && !grid[i][j - 1].hasBarAfter) {
return false;
}
if (grid[i][j].hasBarAfter) {
return false;
}
if (j == gridWidth - 1) {
return false;
}
if (!grid[i][j + 1].isLight) {
return false;
}
return true;
}
function startsDownClue(i, j) {
if (!grid[i][j].isLight) {
return false;
}
if (i > 0 && grid[i - 1][j].isLight && !grid[i - 1][j].hasBarUnder) {
return false;
}
if (grid[i][j].hasBarUnder) {
return false;
}
if (i == gridHeight - 1) {
return false;
}
if (!grid[i + 1][j].isLight) {
return false;
}
return true;
}
// Sets starts{Across,Down}Clue (boolean) and startsClueLabel (#) in
// grid[i][j]s where clues start.
function markClueStartsUsingGrid() {
if (hasDiagramlessCells && hasUnsolvedCells) {
// Cannot rely on grid. Clue starts should be provided in clues using
// prefixes like #a8, #d2, etc.
return
}
let nextClueNumber = 1
for (let i = 0; i < gridHeight; i++) {
for (let j = 0; j < gridWidth; j++) {
if (startsAcrossClue(i, j)) {
grid[i][j].startsAcrossClue = true
grid[i][j].startsClueLabel = '' + nextClueNumber
clues['A' + nextClueNumber] = {'cells': []}
}
if (startsDownClue(i, j)) {
grid[i][j].startsDownClue = true
grid[i][j].startsClueLabel = '' + nextClueNumber
clues['D' + nextClueNumber] = {'cells': []}
}
if (grid[i][j].startsClueLabel) {
nextClueNumber++
}
}
}
}
// If there are any html closing tags, move past them.
function adjustAfterEnum(clueLine, afterEnum) {
let lineAfter = clueLine.substr(afterEnum)
while (lineAfter.trim().substr(0, 2) == '</') {
let closer = clueLine.indexOf('>', afterEnum);
if (closer < 0) {
return afterEnum
}
afterEnum = closer + 1
lineAfter = clueLine.substr(afterEnum)
}
return afterEnum
}
// Parse a cell location in "chess notation" (a1 = bottom-left, etc.) and
// return a two-element array [row, col].
function parseCellLocation(s) {
s = s.trim()
let col = s.charCodeAt(0) - 'a'.charCodeAt(0)
let row = gridHeight - parseInt(s.substr(1))
if (isNaN(row) || isNaN(col) ||
row < 0 || row >= gridHeight || col < 0 || col >= gridWidth) {
return null
}
return [row, col];
}
// Parse an enum like (4) or (4,5), or (5-2,4).
// Return an object with the following properties:
// len
// hyphenAfter[] (0-based indices)
// wordEndAfter[] (0-based indices)
// afterEnum index after enum
function parseEnum(clueLine) {
let parse = {
'len': 0,
'wordEndAfter': [],
'hyphenAfter': [],
'afterEnum': clueLine.length,
};
let enumLocation = clueLine.search(/\([1-9]+[0-9\-,'’\s]*\)/)
if (enumLocation < 0) {
// Look for the the string 'word'/'letter'/? in parens.
enumLocation = clueLine.search(/\([^)]*(word|letter|\?)[^)]*\)/i)
if (enumLocation >= 0) {
let enumEndLocation =
enumLocation + clueLine.substr(enumLocation).indexOf(')')
if (enumEndLocation <= enumLocation) {
return parse
}
parse.afterEnum = adjustAfterEnum(clueLine, enumEndLocation + 1)
}
return parse
}
let enumEndLocation =
enumLocation + clueLine.substr(enumLocation).indexOf(')')
if (enumEndLocation <= enumLocation) {
return parse
}
parse.afterEnum = adjustAfterEnum(clueLine, enumEndLocation + 1)
let enumLeft = clueLine.substring(enumLocation + 1, enumEndLocation)
let nextPart
while (enumLeft && (nextPart = parseInt(enumLeft)) && !isNaN(nextPart) &&
nextPart > 0) {
parse.len = parse.len + nextPart
enumLeft = enumLeft.replace(/\s*\d+\s*/, '')
let nextSymbol = enumLeft.substr(0, 1)
if (nextSymbol == '-') {
parse.hyphenAfter.push(parse.len - 1)
enumLeft = enumLeft.substr(1)
} else if (nextSymbol == ',') {
parse.wordEndAfter.push(parse.len - 1)
enumLeft = enumLeft.substr(1)
} else if (nextSymbol == '\'') {
enumLeft = enumLeft.substr(1)
} else if (enumLeft.indexOf('’') == 0) {
// Fancy apostrophe
enumLeft = enumLeft.substr('’'.length)
} else {
break;
}
}
return parse
}
// Parse a clue label from the start of clueLine.
// Return an object with the following properties:
// error
// isFiller
// clueLabel
// isNonNum
// dir
// hasChildren
// skip
function parseClueLabel(clueLine) {
let parse = {};
parse.dir = ''
parse.hasChilden = false
parse.skip = 0
const numberParts = clueLine.match(/^\s*[1-9]\d*/)
if (numberParts && numberParts.length == 1) {
let clueNum = parseInt(numberParts[0])
parse.clueLabel = '' + clueNum
parse.isNonNum = false
parse.skip = numberParts[0].length
} else {
let bracOpenParts = clueLine.match(/^\s*\[/)
if (!bracOpenParts || bracOpenParts.length != 1) {
parse.isFiller = true
return parse
}
let pastBracOpen = bracOpenParts[0].length
let bracEnd = clueLine.indexOf(']')
if (bracEnd < 0) {
parse.error = 'Missing matching ] in clue label in ' + clueLine
return parse
}
parse.clueLabel = clueLine.substring(pastBracOpen, bracEnd).trim()
let temp = parseInt(parse.clueLabel)
if (!isNaN(temp)) {
parse.error = 'Numeric label not allowed in []: ' + clueLabel
return parse
}
if (parse.clueLabel.charAt(parse.clueLabel.length - 1) == '.') {
// strip trailing period
parse.clueLabel = parse.clueLabel.substr(0, parse.clueLabel.length - 1).trim()
}
parse.isNonNum = true
parse.skip = bracEnd + 1
}
clueLine = clueLine.substr(parse.skip)
const dirParts = clueLine.match(/^[aAdD]/) // no leading space
if (dirParts && dirParts.length == 1) {
parse.dir = dirParts[0].trim().toUpperCase()
parse.skip += dirParts[0].length
clueLine = clueLine.substr(dirParts[0].length)
}
const commaParts = clueLine.match(/^\s*,/)
if (commaParts && commaParts.length == 1) {
parse.hasChildren = true
parse.skip += commaParts[0].length
clueLine = clueLine.substr(commaParts[0].length)
}
// Consume trailing period if it is there.
const periodParts = clueLine.match(/^\s*\./)
if (periodParts && periodParts.length == 1) {
parse.hasChildren = false
parse.skip += periodParts[0].length
clueLine = clueLine.substr(periodParts[0].length)
}
return parse
}
// Parse a single clue.
// Return an object with the following properties:
// clueIndex
// clueLabel
// isNonNum
// children[] (raw parseClueLabel() resutls, not yet clueIndices)
// clue
// len
// hyphenAfter[] (0-based indices)
// wordEndAfter[] (0-based indices)
// startCell[] optional, used in diagramless+unsolved and nonth -numeric labels
// anno (the part after the enum, if present)
// isFiller
// error
function parseClue(dir, clueLine) {
let parse = {};
clueLine = clueLine.trim()
if (clueLine.indexOf('#') == 0) {
let startCell = parseCellLocation(clueLine.substr(1));
if (startCell) {
parse.startCell = startCell
}
clueLine = clueLine.replace(/^#[a-z][0-9]*\s*/, '')
}
let clueLabelParse = parseClueLabel(clueLine)
if (clueLabelParse.error) {
parse.error = clueLabelParse.error
return parse
}
if (clueLabelParse.isFiller) {
parse.isFiller = true
return parse
}
if (clueLabelParse.dir && clueLabelParse.dir != dir) {
parse.error = 'Explicit dir ' + clueLabelParse.dir + ' does not match ' + dir + ' in clue: ' + clueLine
return parse
}
parse.clueLabel = clueLabelParse.clueLabel
parse.isNonNum = clueLabelParse.isNonNum
let clueIndex = dir + parse.clueLabel
if (parse.isNonNum) {
let nonNumIndex = dir + '#' + (nextNonNumId++)
if (!nonNumClueIndices[parse.clueLabel]) {
nonNumClueIndices[parse.clueLabel] = []
}
nonNumClueIndices[parse.clueLabel].push(nonNumIndex)
clueIndex = nonNumIndex
}
parse.clueIndex = clueIndex
clueLine = clueLine.substr(clueLabelParse.skip)
parse.children = []
while (clueLabelParse.hasChildren) {
clueLabelParse = parseClueLabel(clueLine)
if (clueLabelParse.error) {
parse.error = 'Error in linked clue number/label: ' + clueLabelParse.error
return parse
}
parse.children.push(clueLabelParse)
clueLine = clueLine.substr(clueLabelParse.skip)
}
let enumParse = parseEnum(clueLine)
parse.len = enumParse.len
parse.hyphenAfter = enumParse.hyphenAfter
parse.wordEndAfter = enumParse.wordEndAfter
parse.clue = clueLine.substr(0, enumParse.afterEnum).trim()
parse.anno = clueLine.substr(enumParse.afterEnum).trim()
return parse
}
// Parse across and down clues from their exolve sections previously
// identified by parseOverallDisplayMost().
function parseClueLists() {
// Parse across, down, nodir clues
for (let clueDirection of ['A', 'D', 'X']) {
let first, last
if (clueDirection == 'A') {
first = acrossFirstLine
last = acrossLastLine
} else if (clueDirection == 'D') {
first = downFirstLine
last = downLastLine
} else {
first = nodirFirstLine
last = nodirLastLine
}
if (first < 0 || last < first) {
continue
}
let prev = null
let filler = ''
for (let l = first; l <= last; l++) {
let clueLine = puzzleTextLines[l].trim();
if (clueLine == '') {
continue;
}
let clueParse = parseClue(clueDirection, clueLine)
if (clueParse.error) {
addError('Clue parsing error in: ' + clueLine + ': ' + clueParse.error);
return
}
if (clueParse.isFiller) {
filler = filler + clueLine + '\n'
continue
}
if (!clueParse.clueIndex) {
addError('Could not parse clue: ' + clueLine);
return
}
if (clues[clueParse.clueIndex] && clues[clueParse.clueIndex].clue) {
addError('Clue entry already exists for clue: ' + clueLine);
return
}
if (!clues[clueParse.clueIndex]) {
clues[clueParse.clueIndex] = {'cells': []}
}
clues[clueParse.clueIndex].clue = clueParse.clue
clues[clueParse.clueIndex].clueLabel = clueParse.clueLabel
clues[clueParse.clueIndex].isNonNum = clueParse.isNonNum
clues[clueParse.clueIndex].displayLabel = clueParse.clueLabel
clues[clueParse.clueIndex].clueDirection = clueDirection
clues[clueParse.clueIndex].fullDisplayLabel = clueParse.clueLabel
if (clueDirection != 'X' && clueParse.clueLabel) {
clues[clueParse.clueIndex].fullDisplayLabel =
clues[clueParse.clueIndex].fullDisplayLabel + clueDirection.toLowerCase()
}
clues[clueParse.clueIndex].children = clueParse.children
clues[clueParse.clueIndex].childrenClueIndices = []
clues[clueParse.clueIndex].len = clueParse.len
clues[clueParse.clueIndex].hyphenAfter = clueParse.hyphenAfter
clues[clueParse.clueIndex].wordEndAfter = clueParse.wordEndAfter
clues[clueParse.clueIndex].anno = clueParse.anno
if (clueParse.anno) {
hasSomeAnnos = true
}
if (clueParse.startCell) {
let row = clueParse.startCell[0]
let col = clueParse.startCell[1]
grid[row][col].startsClueLabel = clueParse.clueLabel
grid[row][col].forcedClueLabel = true
if (clueDirection == 'A') {
grid[row][col].startsAcrossClue = true
} else if (clueDirection == 'D') {
grid[row][col].startsDownClue = true
}
}
clues[clueParse.clueIndex].prev = prev
clues[clueParse.clueIndex].next = null
if (prev) {
clues[prev].next = clueParse.clueIndex
}
prev = clueParse.clueIndex
if (filler) {
clues[clueParse.clueIndex].filler = filler
filler = ''
}
if (clueParse.clue) {
allClueIndices.push(clueParse.clueIndex)
}
}
if (filler) {
addError('Filler line should not be at the end: ' + filler)
return
}
}
}
// For each cell grid[i][j], set {across,down}ClueLabels using previously
// marked clue starts. Adds clues to orphanClueIndices[] if warranted.
function setClueMemberships() {
// Set across clue memberships
for (let i = 0; i < gridHeight; i++) {
let clueLabel = ''
for (let j = 0; j < gridWidth; j++) {
if (grid[i][j].startsAcrossClue) {
clueLabel = grid[i][j].startsClueLabel
}
if (!clueLabel) {
continue
}
if (!grid[i][j].isLight || grid[i][j].isDiagramless) {
clueLabel = '';
continue
}
if (!grid[i][j].startsAcrossClue && j > 0 && grid[i][j - 1].hasBarAfter) {
clueLabel = '';
continue
}
grid[i][j].acrossClueLabel = clueLabel
let clueIndex = 'A' + clueLabel
if (!clues[clueIndex]) {
clueIndex = 'X' + clueLabel
}
if (!clues[clueIndex]) {
if (!nonNumClueIndices[clueLabel]) {
clueLabel = ''
continue
}
clueIndex = ''
for (ci of nonNumClueIndices[clueLabel]) {
if (ci.charAt(0) == 'A' || ci.charAt(0) == 'X') {
clueIndex = ci
break
}
}
if (!clueIndex) {
clueLabel = ''
continue
}
}
clues[clueIndex].cells.push([i, j])
}
}
// Set down clue memberships
for (let j = 0; j < gridWidth; j++) {
let clueLabel = ''
for (let i = 0; i < gridHeight; i++) {
if (grid[i][j].startsDownClue) {
clueLabel = grid[i][j].startsClueLabel
}
if (!clueLabel) {
continue
}
if (!grid[i][j].isLight || grid[i][j].isDiagramless) {
clueLabel = '';
continue
}
if (!grid[i][j].startsDownClue && i > 0 && grid[i - 1][j].hasBarUnder) {
clueLabel = '';
continue
}
grid[i][j].downClueLabel = clueLabel
let clueIndex = 'D' + clueLabel
if (!clues[clueIndex]) {
clueIndex = 'X' + clueLabel
}
if (!clues[clueIndex]) {
if (!nonNumClueIndices[clueLabel]) {
clueLabel = ''
continue
}
clueIndex = ''
for (ci of nonNumClueIndices[clueLabel]) {
if (ci.charAt(0) == 'D' || ci.charAt(0) == 'X') {
clueIndex = ci
break
}
}
if (!clueIndex) {
clueLabel = ''
continue
}
}
clues[clueIndex].cells.push([i, j])
}
}
for (let clueIndex of allClueIndices) {
if (!clues[clueIndex].cells || !clues[clueIndex].len ||
!clues[clueIndex].cells.length) {
orphanClueIndices.push(clueIndex)
}
}
}
// For clues that have "child" clues (indicated like, '2, 13, 14' for parent 2,
// child 13, child 14), save the parent-child relationships, and successor grid
// cells for last cells in component clues, and spilled-over hyphenAfter and
// wordEndAfter locations.
function processClueChildren() {
for (let clueIndex of allClueIndices) {
let clue = clues[clueIndex]
if (!clue.children) {
continue
}
// Process children
// We also need to note the successor of he last cell from the parent
// to the first child, and then from the first child to the next, etc.
let lastRowCol = null
if (clue.cells.length > 0) {
lastRowCol = clue.cells[clue.cells.length - 1]
// If we do not know the enum of this clue (likely a diagramless puzzle),
// do not set successors.
if (!clue.len || clue.len <= 0) {
lastRowCol = null
}
}
let lastRowColDir = clue.clueDirection
const dupes = {}
const allDirections = ['A', 'D', 'X']
for (let child of clue.children) {
if (child.error) {
addError('Bad child ' + child + ' in ' +
clue.cluelabel + clue.clueDirection);
return
}
// Direction could be the same as the direction of the parent. Or,
// if there is no such clue, then direction could be the other direction.
// The direction could also be explicitly specified with a 'd' or 'a'
// suffix.
let childIndex = clue.clueDirection + child.clueLabel
if (!child.isNonNum) {
if (!clues[childIndex]) {
for (let otherDir of allDirections) {
if (otherDir == clue.clueDirection) {
continue;
}
childIndex = otherDir + child.clueLabel
if (clues[childIndex]) {
break
}
}
}
if (child.dir) {
childIndex = child.dir + child.clueLabel
}
} else {
if (!nonNumClueIndices[child.clueLabel] ||
nonNumClueIndices[child.clueLabel].length < 1) {
addError('non-num child label ' + child.clueLabel + ' was not seen')
return
}
childIndex = nonNumClueIndices[child.clueLabel][0]
}
if (!clues[childIndex] || childIndex == clueIndex) {
addError('Invalid child ' + childIndex + ' in ' +
clue.cluelabel + clue.clueDirection);
return
}
if (dupes[childIndex]) {
addError('Duplicate child ' + childIndex + ' in ' +
clue.cluelabel + clue.clueDirection);
return
}
dupes[childIndex] = true
if (child.clueLabel) {
clue.displayLabel = clue.displayLabel + ', ' + child.clueLabel
if (child.dir && child.dir != clue.clueDirection) {
clue.displayLabel = clue.displayLabel + child.dir.toLowerCase()
}
clue.fullDisplayLabel = clue.fullDisplayLabel + ', ' + child.clueLabel
if (childIndex.charAt(0) != 'X') {
clue.fullDisplayLabel = clue.fullDisplayLabel + childIndex.charAt(0).toLowerCase()
}
}
clue.childrenClueIndices.push(childIndex)
let childClue = clues[childIndex]
childClue.parentClueIndex = clueIndex
if (lastRowCol && childClue.cells.length > 0) {
let cell = childClue.cells[0]
if (lastRowCol[0] == cell[0] && lastRowCol[1] == cell[1]) {
addError('loop in successor for ' + lastRowCol)
return
} else {
grid[lastRowCol[0]][lastRowCol[1]]['successor' + lastRowColDir] = {
'cell': cell,
'direction': childClue.clueDirection
};
}
}
lastRowCol = null
if (childClue.cells.length > 0) {
lastRowCol = childClue.cells[childClue.cells.length - 1]
if (!childClue.len || childClue.len <= 0) {
lastRowCol = null
}
}
lastRowColDir = childClue.clueDirection
}
if (hasDiagramlessCells) {
continue
}
// If clue.wordEndAfter[] or clue.hyphenAfter() spill into children,
// then copy the appropriate parts there.
let prevLen = clue.cells.length
let wordEndIndex = 0
while (wordEndIndex < clue.wordEndAfter.length &&
clue.wordEndAfter[wordEndIndex] < prevLen) {
wordEndIndex++;
}
let hyphenIndex = 0
while (hyphenIndex < clue.hyphenAfter.length &&
clue.hyphenAfter[hyphenIndex] < prevLen) {
hyphenIndex++;
}
for (let childIndex of clue.childrenClueIndices) {
let childLen = clues[childIndex].cells.length
while (wordEndIndex < clue.wordEndAfter.length &&
clue.wordEndAfter[wordEndIndex] < prevLen + childLen) {
let pos = clue.wordEndAfter[wordEndIndex] - prevLen
clues[childIndex].wordEndAfter.push(pos)
wordEndIndex++
}
while (hyphenIndex < clue.hyphenAfter.length &&
clue.hyphenAfter[hyphenIndex] < prevLen + childLen) {
let pos = clue.hyphenAfter[hyphenIndex] - prevLen
clues[childIndex].hyphenAfter.push(pos)
hyphenIndex++
}
prevLen = prevLen + childLen
}
}
}
// Place a trailing period and space at the end of clue full display labels that
// are not empty.
function fixFullDisplayLabels() {
for (let clueIndex of allClueIndices) {
if (clues[clueIndex].fullDisplayLabel) {
clues[clueIndex].fullDisplayLabel = clues[clueIndex].fullDisplayLabel + '. '
}
}
}
// Using hyphenAfter[] and wordEndAfter[] in clues, set
// {hyphen,wordEnd}{ToRight,Below} in grid[i][j]s.
function setGridWordEndsAndHyphens() {
if (hasDiagramlessCells) {
// Give up on this
return
}
// Going across
for (let i = 0; i < gridHeight; i++) {
let clueLabel = ''
let clueIndex = ''
let positionInClue = -1
for (let j = 0; j < gridWidth; j++) {
if (!grid[i][j].acrossClueLabel) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
if (clueLabel == grid[i][j].acrossClueLabel) {
positionInClue++
} else {
clueLabel = grid[i][j].acrossClueLabel
positionInClue = 0
clueIndex = 'A' + clueLabel
if (!clues[clueIndex]) {
if (!nonNumClueIndices[clueLabel]) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
for (ci of nonNumClueIndices[clueLabel]) {
if (ci.charAt(0) == 'A' || ci.charAt(0) == 'X') {
clueIndex = ci
break
}
}
}
if (!clues[clueIndex] || !clues[clueIndex].clue) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
}
for (let wordEndPos of clues[clueIndex].wordEndAfter) {
if (positionInClue == wordEndPos && j < gridWidth - 1) {
grid[i][j].wordEndToRight = true
break
}
}
for (let hyphenPos of clues[clueIndex].hyphenAfter) {
if (positionInClue == hyphenPos && j < gridWidth - 1) {
grid[i][j].hyphenToRight = true
break
}
}
}
}
// Going down
for (let j = 0; j < gridWidth; j++) {
let clueLabel = ''
let clueIndex = ''
let positionInClue = -1
for (let i = 0; i < gridHeight; i++) {
if (!grid[i][j].downClueLabel) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
if (clueLabel == grid[i][j].downClueLabel) {
positionInClue++
} else {
clueLabel = grid[i][j].downClueLabel
positionInClue = 0
clueIndex = 'D' + clueLabel
if (!clues[clueIndex]) {
if (!nonNumClueIndices[clueLabel]) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
for (ci of nonNumClueIndices[clueLabel]) {
if (ci.charAt(0) == 'D' || ci.charAt(0) == 'X') {
clueIndex = ci
break
}
}
}
if (!clues[clueIndex] || !clues[clueIndex].clue) {
clueLabel = ''
clueIndex = ''
positionInClue = -1
continue
}
}
for (let wordEndPos of clues[clueIndex].wordEndAfter) {
if (positionInClue == wordEndPos && i < gridHeight - 1) {
grid[i][j].wordEndBelow = true
break
}
}
for (let hyphenPos of clues[clueIndex].hyphenAfter) {
if (positionInClue == hyphenPos && i < gridHeight - 1) {
grid[i][j].hyphenBelow = true
break
}
}
}
}
}
function stripLineBreaks(s) {
s = s.replace(/<br\s*\/?>/gi, " / ")
return s.replace(/<\/br\s*>/gi, "")
}
function displayClues() {
// Populate clues tables. Check that we have all clues
for (let clueIndex of allClueIndices) {
if (!clues[clueIndex].clue && !clues[clueIndex].parentClueIndex) {
addError('Found no clue text nor a parent clue for ' + clueIndex)
return
}
let table = null
if (clues[clueIndex].clueDirection == 'A') {
table = acrossClues
hasAcrossClues = true
} else if (clues[clueIndex].clueDirection == 'D') {
table = downClues
hasDownClues = true
} else if (clues[clueIndex].clueDirection == 'X') {
table = nodirClues
hasNodirClues = true
} else {
addError('Unexpected clue direction ' + clues[clueIndex].clueDirection + ' in ' + clueIndex)
return
}
if (clues[clueIndex].filler) {
let tr = document.createElement('tr')
let col = document.createElement('td')
col.setAttributeNS(null, 'colspan', '2');
col.setAttributeNS(null, 'class', 'filler');
col.innerHTML = clues[clueIndex].filler
tr.appendChild(col)
table.appendChild(tr)
}
let tr = document.createElement('tr')
let col1 = document.createElement('td')
col1.innerHTML = `<div class="clue-label">${clues[clueIndex].displayLabel}</div>`;
let col2 = document.createElement('td')
col2.innerHTML = clues[clueIndex].clue
// If clue contains <br> tags, replace them with "/" for future renderings
// in the "current clue" strip.
if (clues[clueIndex].clue.indexOf('<') >= 0) {
clues[clueIndex].clue = stripLineBreaks(clues[clueIndex].clue)
}
if (clues[clueIndex].anno) {
let anno = document.createElement('span')
anno.setAttributeNS(null, 'class', 'anno-text');
anno.innerHTML = ' ' + clues[clueIndex].anno
anno.style.display = 'none'
revelationList.push(anno)
col2.appendChild(anno)
clues[clueIndex].annoSpan = anno
}
tr.appendChild(col1)
tr.appendChild(col2)
if (clues[clueIndex].cells.length > 0) {
let i = clues[clueIndex].cells[0][0]
let j = clues[clueIndex].cells[0][1]
tr.addEventListener('click', getRowColDirActivator(
i, j, clues[clueIndex].clueDirection));
} else {
// Fully diagramless. Just select clue.
tr.addEventListener('click', getClueSelector(clueIndex));
}
clues[clueIndex].clueTR = tr
table.appendChild(tr)
}
if (cluesPanelLines > 0) {
const ems = 1.40 * cluesPanelLines
const emsStyle = '' + ems + 'em'
acrossPanel.style.height = emsStyle
downPanel.style.height = emsStyle
if (nodirPanel) {
nodirPanel.style.height = emsStyle
}
}
if (hasAcrossClues) {
acrossPanel.style.display = ''
}
if (hasDownClues) {
downPanel.style.display = ''
}
if (hasNodirClues) {
nodirPanel.style.display = ''
}
}
function displayGridBackground() {
svg.setAttributeNS(null, 'viewBox', '0 0 ' + boxWidth + ' ' + boxHeight)
svg.setAttributeNS(null, 'width', boxWidth);
svg.setAttributeNS(null, 'height', boxHeight);
background.setAttributeNS(null, 'x', 0);
background.setAttributeNS(null, 'y', 0);
background.setAttributeNS(null, 'width', boxWidth);
background.setAttributeNS(null, 'height', boxHeight);
background.setAttributeNS(null, 'class', 'background');
svg.appendChild(background);
}
// Return a string encoding the current entries in the whole grid and
// also the number of squares that have been filled.
function getGridStateAndNumFilled() {
let state = '';
let numFilled = 0
for (let i = 0; i < gridHeight; i++) {
for (let j = 0; j < gridWidth; j++) {
if (grid[i][j].isLight || grid[i][j].isDiagramless) {
let letter = grid[i][j].currentLetter.trim()
if (letter == '') {
state = state + '0'
} else {
state = state + letter
numFilled++
}
} else {
state = state + '.'
}
}
}
return [state, numFilled];
}
// Update status, ensure answer fields are upper-case (when they have
// an enum), disable buttons as needed, and return the state.
function updateDisplayAndGetState() {
let stateAndFilled = getGridStateAndNumFilled();
let state = stateAndFilled[0]
let numFilled = stateAndFilled[1]
statusNumFilled.innerHTML = numFilled
for (let a of answersList) {
if (a.hasEnum) {
a.input.value = a.input.value.toUpperCase()
}
}
clearButton.disabled = (activeCells.length == 0)
checkButton.disabled = (activeCells.length == 0)
revealButton.disabled = (activeCells.length == 0)
submitButton.disabled = (numFilled != numCellsToFill)
return state
}
// Call updateDisplayAndGetState() and save state in cookie and location.hash.
function updateAndSaveState() {
let state = updateDisplayAndGetState()
for (let a of answersList) {
state = state + STATE_SEP + a.input.value
}
// Keep cookie for these many days
const KEEP_FOR_DAYS = 90
let d = new Date();
d.setTime(d.getTime() + (KEEP_FOR_DAYS * 24 * 60 * 60 * 1000));
let expires = 'expires=' + d.toUTCString();
document.cookie = puzzleId + '=' + state + ';' + expires + ';path=/';
let oldState = decodeURIComponent(location.hash.substr(1))
let parts = oldState.split('?');
let newParts = [];
let newPartsForSaving = [];
let found = false;
for (const part in parts) {
const partString = parts[part];
if (partString.indexOf(exolvePrefix) === 0) {
const eqIndex = partString.indexOf('=');
const k