/* MIT License Copyright (c) 2020 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 Exet can be found at: https://github.com/viresh-ratnakar/exet Current version: v0.91, January 9, 2024 */ function ExetModals() { this.modal = null; document.addEventListener('click', this.handleClick.bind(this)); document.addEventListener('keydown', this.handleClick.bind(this)); }; ExetModals.prototype.handleClick = function(e) { if (!this.modal) { return } if (!this.modal.contains(e.target) || e.key == "Escape") { this.hide() } } // If caller calls this in response to a click event e, then caller should also // call e.stopPropagation(). ExetModals.prototype.showModal = function(elt) { this.hide() if (!elt) { return } this.modal = elt; this.modal.style.display = 'block' } ExetModals.prototype.hide = function() { if (!this.modal) { return } if (exet.postscript && this.modal.id == 'xet-other-sections') { exet.postscript.style.display = 'none'; } this.modal.style.display = 'none' this.modal = null; } function ExetRevManager() { this.REV_LOADED_FROM_FILE = 1 this.REV_CREATED_BLANK = 2 this.REV_CREATED_AUTOBLOCK = 3 this.REV_JUMPED_TO_REV = 10 this.REV_GRID_CHANGE = 20 this.REV_LIGHT_REVERSAL = 24 this.REV_AUTOFILL_GRIDFILL_CHANGE = 28 this.REV_GRIDFILL_CHANGE = 30 this.REV_ENUM_CHANGE = 40 this.REV_CLUE_CHANGE = 50 this.REV_METADATA_CHANGE = 60 this.REV_PREFLEX_CHANGE = 70 this.REV_OPTIONS_CHANGE = 80 this.revMsgs = {} this.revMsgs[this.REV_LOADED_FROM_FILE] = "Loaded from a file" this.revMsgs[this.REV_CREATED_BLANK] = "Created a blank grid" this.revMsgs[this.REV_CREATED_AUTOBLOCK] = "Created a blank grid " + "with automagic blocks" this.revMsgs[this.REV_JUMPED_TO_REV] = "Jumped to a previous revision" this.revMsgs[this.REV_GRID_CHANGE] = "Grid change" this.revMsgs[this.REV_LIGHT_REVERSAL] = "Light reversal" this.revMsgs[this.REV_AUTOFILL_GRIDFILL_CHANGE] = "Autofilled grid-fill " + "change" this.revMsgs[this.REV_GRIDFILL_CHANGE] = "Grid-fill change" this.revMsgs[this.REV_ENUM_CHANGE] = "Enum change" this.revMsgs[this.REV_CLUE_CHANGE] = "Clue or anno change" this.revMsgs[this.REV_METADATA_CHANGE] = "Metadata change" this.revMsgs[this.REV_PREFLEX_CHANGE] = "Change in the list of or options " + "for preferred words" this.revMsgs[this.REV_OPTIONS_CHANGE] = "Crossword options change" /* State for throttled revision-saving */ this.throttleRevTimer = null; this.saveLagMS = 5000 this.throttlingLastRev = 0; /** * Special localStorage key for storing preferences and state, * qualified by non-default lexicon properties. */ this.SPECIAL_KEY_PREFIX = '42-exet-42'; this.SPECIAL_KEY = this.SPECIAL_KEY_PREFIX; if ('en' != exetLexicon.language || 'Latin' != exetLexicon.script || 1 != exetLexicon.maxCharCodes) { this.SPECIAL_KEY += `-${exetLexicon.language}-${exetLexicon.script}` + `-${exetLexicon.maxCharCodes}`; } this.spaceUsedAtStart = 0 for (let idx = 0; idx < window.localStorage.length; idx++) { let id = window.localStorage.key(idx) this.spaceUsedAtStart += window.localStorage.getItem(id).length } this.spaceUsed = this.spaceUsedAtStart let k500 = '1234567812345678' while (k500.length < 500000) { k500 = k500 + k500 } let tempKey = '42-exet-cap-42-' this.spaceLeftAtStart = 0 for (let i = 0; i < 20; i++) { // Only count up to 10 MB try { window.localStorage.setItem(tempKey + i, k500) this.spaceLeftAtStart += k500.length } catch (err) { break } } for (let i = 0; i < 20; i++) { window.localStorage.removeItem(tempKey + i) } // Id for previews this.previewId = `exet-preview-${Math.random().toString(36).substring(2, 8)}` }; ExetRevManager.prototype.inMB = function(num) { return (num / 1000000).toFixed(2) } ExetRevManager.prototype.choosePuzRev = function(manageStorage, puz, elt, callback) { let choices = []; if (puz) { let stored = window.localStorage.getItem(puz.id); let spaceUsed = stored.length; choices = [{id: puz.id, title: puz.title, space: spaceUsed}]; } else { this.spaceUsed = 0; for (let idx = 0; idx < window.localStorage.length; idx++) { let id = window.localStorage.key(idx); let stored = window.localStorage.getItem(id); let spaceUsed = stored.length; this.spaceUsed += spaceUsed; if (id.startsWith(this.SPECIAL_KEY_PREFIX)) { continue; } try { stored = JSON.parse(stored); } catch (err) { continue; } if (!stored || !stored["id"] || !stored["revs"] || !stored["maxRevNum"]) { continue; } let title = ''; if (stored.revs.length > 0) { title = stored.revs[stored.revs.length - 1].title; } choices.push({id: stored.id, title: title, space: spaceUsed}); } } const storageUsedMB = this.inMB(this.spaceUsed); const storageFreeMB = this.inMB(this.spaceUsedAtStart + this.spaceLeftAtStart - this.spaceUsed); exet.storageUsed.innerText = storageUsedMB; exet.storageFree.innerText = storageFreeMB; let html = `
Select puzzle ID/Title Select revision
` for (let i = 0; i < choices.length; i++) { html = html + ` ` } html = html + `
${choices[i].id} ${choices[i].title} ${this.inMB(choices[i].space)} MB
Space used: ${storageUsedMB} MB Space available: ${storageFreeMB} MB
` elt.innerHTML = html this.idChoicesBox = document.getElementById('xet-choose-id') this.idChoicesBox.style.width = '270px' this.idChoicesBox.style.height = '200px' this.idChoices = document.getElementById('xet-id-choices') this.revChoicesBox = document.getElementById('xet-choose-rev') this.revChoicesBox.style.width = '500px' this.revChoicesBox.style.height = '200px' this.revChoices = document.getElementById('xet-rev-choices') this.preview = document.getElementById('xet-preview') this.idChoice = '' this.revChoice = -1 this.puzDeleter = document.getElementById('xet-puz-deleter') this.puzPriorDeleter = document.getElementById('xet-puz-prior-deleter') this.puzRevDeleter = document.getElementById('xet-puz-rev-deleter') this.puzRevSelector = document.getElementById('xet-puz-rev-selector') this.manageStorage = manageStorage if (manageStorage) { this.puzDeleter.style.display = '' this.puzPriorDeleter.style.display = '' this.puzRevDeleter.style.display = '' this.puzDeleter.disabled = true this.puzPriorDeleter.disabled = true this.puzRevDeleter.disabled = true let deleter = (types, e) => { if (!confirm('Are you sure you want to delete ' + types + ' revision(s)?')) { return } this.idChoices.className = 'xet-choices' this.revChoices.className = 'xet-choices' if (types == 'all') { window.localStorage.removeItem(this.idChoice) } else { if (this.revChoice < 0 || !this.storedRevs || this.storedRevs.revs.length == 0 || this.revChoice >= this.storedRevs.revs.length) { console.log('Weird, did not find revChoice/storedRevs to delete from') return } let lastToDelete = this.revChoice if (types == 'prior') lastToDelete-- let numToDelete = (types == 'prior' ? lastToDelete + 1 : 1) let newRevs = [] if (lastToDelete - numToDelete >= 0) { newRevs = this.storedRevs.revs.slice( 0, lastToDelete - numToDelete + 1) } this.storedRevs.revs = newRevs.concat( this.storedRevs.revs.slice(lastToDelete + 1)) this.saveLocal(this.idChoice, JSON.stringify(this.storedRevs)) } this.choosePuzRev(true, null, exet.revChooser, null) e.stopPropagation() } this.puzDeleter.addEventListener('click', deleter.bind(this, 'all')) this.puzPriorDeleter.addEventListener('click', deleter.bind(this, 'prior')) this.puzRevDeleter.addEventListener('click', deleter.bind(this, 'this')) } else { this.puzRevSelector.style.display = '' this.puzRevSelector.disabled = true this.puzRevSelector.addEventListener('click', e => { if (this.revChoice < 0 || !this.storedRevs || this.storedRevs.revs.length == 0 || this.revChoice >= this.storedRevs.revs.length) { console.log('Hmm: bad selection! Check ExetRevManager:') console.log(this) return } exetModals.hide() this.idSelectors = [] this.revSelectors = [] this.preview.innerHTML = '' if (exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } callback(this.storedRevs.revs[this.revChoice]) }) } this.idSelectors = [] this.revSelectors = [] this.storedRevs = null if (puz) { this.idChoice = puz.id document.getElementById("xet-id-choice-0").className = 'xet-chosen' this.chooseRev() return } for (let i = 0; i < choices.length; i++) { let selector = document.getElementById(`xet-id-choice-${i}`) this.idSelectors.push(selector) let id = choices[i].id selector.addEventListener('click', e => { this.preview.innerHTML = '' if (exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } this.puzDeleter.disabled = true this.puzPriorDeleter.disabled = true this.puzRevDeleter.disabled = true this.revChoices.innerHTML = '' this.revChoices.className = 'xet-choices' this.revChoice = -1 this.revSelectors = [] this.storedRevs = null this.puzRevSelector.disabled = true if (id == this.idChoice) { this.idChoice = null selector.className = '' this.idChoices.className = 'xet-choices' } else { for (let j = 0; j < choices.length; j++) { if (j != i) { this.idSelectors[j].className = '' } } this.idChoice = id this.puzDeleter.disabled = false selector.className = 'xet-chosen' this.idChoices.className = 'xet-choices xet-picked' this.chooseRev() } }) } }; Exet.prototype.renderPreview = function(spec, eltId) { try { let newPuz = new Exolve(spec, eltId, null, false, 0, 400, false) document.getElementById( `${newPuz.prefix}-controls-etc`).style.display = 'none'; document.getElementById( `${newPuz.prefix}-clear-area`).style.display = 'none' newPuz.revealAll(false) } catch (err) { console.log(err); } } ExetRevManager.prototype.chooseRev = function() { let stored = window.localStorage.getItem(this.idChoice) if (!stored) { return } this.storedRevs = JSON.parse(stored) let html = '' for (let idx = this.storedRevs.revs.length - 1; idx >= 0; idx--) { let rev = this.storedRevs.revs[idx] let revTime = new Date(rev.timestamp) html = html + ` ${rev.title} #${rev.revNum} ${revTime.toLocaleString()} ${exetRevManager.revMsgs[rev.revType]} ${rev.details} ` } this.revChoices.innerHTML = html this.revSelectors = [] this.revChoice = -1 for (let i = 0; i < this.storedRevs.revs.length; i++) { let selector = document.getElementById(`xet-rev-choice-${i}`) this.revSelectors.push(selector) selector.addEventListener('click', e => { if (!this.storedRevs) { return } this.puzPriorDeleter.disabled = true this.puzRevDeleter.disabled = true this.puzRevSelector.disabled = true this.preview.innerHTML = '' if (exolvePuzzles[this.previewId]) { exolvePuzzles[this.previewId].destroy(); } if (i == this.revChoice) { this.revChoice = -1 selector.className = '' this.revChoices.className = 'xet-choices' } else { for (let j = 0; j < this.revSelectors.length; j++) { if (j != i) { this.revSelectors[j].className = '' } } this.revChoice = i selector.className = 'xet-chosen' this.revChoices.className = 'xet-choices xet-picked' let exolve = this.storedRevs.revs[i].exolve.replace( /exolve-id:[^\n]*/, `exolve-id: ${this.previewId}`) exet.renderPreview(exolve, "xet-preview") this.puzPriorDeleter.disabled = (i <= 0) this.puzRevDeleter.disabled = false this.puzRevSelector.disabled = false } }) } }; ExetRevManager.prototype.saveLocal = function(k, v) { try { window.localStorage.setItem(k, v) } catch (err) { alert('No available local storage left. Please use the ' + '"Manage local storage" menu option to free up some space.'); console.log('Could not save value of length ' + v.length + ' for key: ' + k) } } ExetRevManager.prototype.saveRev = function(revType, details="") { if (!exet || !exet.puz || !exet.puz.id) { console.log('Cannot save revision when there is no puzzle!') return } let stored = window.localStorage.getItem(exet.puz.id) if (!stored) { stored = { id: exet.puz.id, maxRevNum: 0, revs: [] } } else { stored = JSON.parse(stored) } let exolve = exet.getExolve() if (stored.revs.length > 0) { let lastRev = stored.revs[stored.revs.length - 1] if (lastRev.exolve == exolve && lastRev.prefix == exet.prefix && lastRev.suffix == exet.suffix && lastRev.scratchPad == exet.puz.scratchPad.value && lastRev.preflex && JSON.stringify(lastRev.preflex) == JSON.stringify(exet.preflex) && lastRev.unpreflex && JSON.stringify(lastRev.unpreflex) == JSON.stringify(exet.unpreflex) && lastRev.noProperNouns == exet.noProperNouns && lastRev.tryReversals == exet.tryReversals && lastRev.minpop == exet.minpop && lastRev.asymOK == exet.asymOK) { return } } stored.maxRevNum++; let exetRev = new ExetRev(exet.puz.id, (exet.puz.title ? exet.puz.title : ''), stored.maxRevNum, revType, Date.now(), details) exetRev.maxRevNum = stored.maxRevNum exetRev.prefix = exet.prefix exetRev.suffix = exet.suffix exetRev.exolve = exolve exetRev.scratchPad = exet.puz.scratchPad.value exetRev.navState = [exet.puz.currDir, exet.puz.currRow, exet.puz.currCol] exetRev.preflex = exet.preflex exetRev.unpreflex = exet.unpreflex exetRev.noProperNouns = exet.noProperNouns exetRev.asymOK = exet.asymOK exetRev.tryReversals = exet.tryReversals exetRev.minpop = exet.minpop stored.revs.push(exetRev) this.saveLocal(exet.puz.id, JSON.stringify(stored)) }; ExetRevManager.prototype.throttledSaveRev = function(revType, details="") { let urgent = revType <= 10; if (this.throttleRevTimer) { clearTimeout(this.throttleRevTimer); if (this.throttlingRevType > 0 && revType < this.throttlingRevType) { urgent = true } } this.throttleRevTimer = null; this.throttlingRevType = 0; if (urgent) { this.saveRev(revType, details) return } this.throttlingRevType = revType; this.throttleRevTimer = setTimeout(() => { this.saveRev(revType, details) this.throttleRevTimer = null; this.throttlingRevType = 0; }, this.saveLagMS); } ExetRevManager.prototype.saveAllRevisions = function() { const storage = {} for (let idx = 0; idx < window.localStorage.length; idx++) { let id = window.localStorage.key(idx) if (id.startsWith(this.SPECIAL_KEY_PREFIX)) { continue } let storedRevsBlob = window.localStorage.getItem(id) let storedRevs = null try { storedRevs = JSON.parse(storedRevsBlob) } catch (err) { continue } if (!storedRevs || !storedRevs['revs']) { continue } storage[id] = storedRevs } const json = JSON.stringify(storage, null, 2) const a = document.createElement('a'); a.style.display = 'none'; document.body.appendChild(a); a.href = window.URL.createObjectURL( new Blob([json], {type: 'text/json'}) ); let filename = `exet-revisions-${(new Date()).toISOString()}.json` a.setAttribute('download', filename) a.click(); window.URL.revokeObjectURL(a.href); document.body.removeChild(a); exetModals.hide() } ExetRevManager.prototype.mergeRevisionsFile = function() { exetModals.hide() let fr = new FileReader(); fr.onload = function(){ let allSavedRevs = {} try { allSavedRevs = JSON.parse(fr.result) } catch (err) { alert('Could not parse the saved revisions file') return } existingRevs = {} for (let idx = 0; idx < window.localStorage.length; idx++) { let id = window.localStorage.key(idx) if (id.startsWith(this.SPECIAL_KEY_PREFIX)) { continue } let storedRevsBlob = window.localStorage.getItem(id) let storedRevs = null try { storedRevs = JSON.parse(storedRevsBlob) } catch (err) { continue } if (!storedRevs || !storedRevs['revs']) { continue } for (rev of storedRevs['revs']) { const revHash = exetLexicon.javaHash(JSON.stringify(rev)) existingRevs[revHash] = true } } let numRevs = 0 let numRevsMerged = 0 let numDupRevs = 0 let numNonLatest = 0 const mergeOnlyLatest = document.getElementById( 'xet-merge-only-latest-revs').checked ? true : false; for (let id in allSavedRevs) { savedRevs = allSavedRevs[id]['revs'] if (!savedRevs || savedRevs.length == 0) { continue } const start = mergeOnlyLatest ? savedRevs.length - 1 : 0 if (mergeOnlyLatest) { numNonLatest += savedRevs.length - 1 } revsToSplice = [] for (let i = start; i < savedRevs.length; i++) { numRevs++ const rev = savedRevs[i] const revHash = exetLexicon.javaHash(JSON.stringify(rev)) if (existingRevs[revHash]) { numDupRevs++ continue } revsToSplice.push(rev) } if (revsToSplice.length == 0) { continue } let stored = window.localStorage.getItem(id) if (stored) { try { stored = JSON.parse(stored) } catch (err) { console.log('Skipped id in merging as JSON.parse() failed, id: ' + id) continue; } } if (!stored) { stored = { id: id, maxRevNum: 0, revs: [] } } for (rev of revsToSplice) { stored['revs'].push(rev) } stored['revs'].sort((r1, r2) => r1.timestamp - r2.timestamp); for (rev of stored['revs']) { if (rev.revNum > stored.maxRevNum) { stored.maxRevNum = rev.revNum } } exetRevManager.saveLocal(id, JSON.stringify(stored)) numRevsMerged += revsToSplice.length } const ignored = (numNonLatest > 0) ? `Ignored ${numNonLatest} non-latest revisions.` : ''; alert(`From ${numRevs} revisions considered across ` + `${Object.keys(allSavedRevs).length} crosswords, merged ` + `${numRevsMerged} revisions. There were ${numDupRevs} revisions ` + `that already existed. ${ignored}`); } let f = document.getElementById('xet-merge-revs-file').files[0] fr.readAsText(f) } function ExetRev(id, title, revNum, revType, timestamp, details="") { this.id = id; this.title = title this.revNum = revNum; this.revType = revType; this.timestamp = timestamp; this.details = details; // prefix, suffix, exolve should be set directly. }; function Exet() { this.version = 'v0.91, January 9, 2024'; this.puz = null; this.prefix = ''; this.suffix = ''; this.exolveOtherSec = ''; this.preflex = []; this.preflexInUse = {}; this.unpreflex = {}; this.noProperNouns = false; this.asymOK = false; this.tryReversals = false; this.DEFAULT_MINPOP = exetConfig.defaultPopularity; this.setMinPop(this.DEFAULT_MINPOP) this.DRAFT = '[DRAFT]'; this.CLUE_NOT_SET = 'Set clue and clear draft marker...'; this.MENU_SEPARATOR = ''; this.TOP_CLEARANCE = 36; this.formatTags = { clear: {inClue: [true, false]}, def: {open: '~{', close: '}~', inClue: [true]}, i: {open: '', close: '', inClue: [true, false]}, b: {open: '', close: '', inClue: [false]}, u: {open: '', close: '', inClue: [false]}, s: {open: '', close: '', inClue: [false]}, caps: {inClue: [false]}, alt: {inClue: [false]}, } // Start in the Exet tab this.currTab = "exet" this.savedIndsSelect = "" // State for throttled handlers this.throttledGridTimer = null; this.throttledPreflexTimer = null; this.throttledUnpreflexTimer = null; this.throttledClueTimer = null; this.throttledMetadataTimer = null; this.throttledCharadeTimer = null; this.viabilityUpdateTimer = null; this.inputLagMS = 400 this.longInputLagMS = 2000 this.sweepMS = 500 // Params for light choices shown. this.sweepMaxChoices = 5000 this.sweepMaxChoicesSmall = 4 this.shownLightChoices = 200 this.tipsList = [ `If you want to allow enum mismatches, then add the line
exolve-option: ignore-enum-mismatch
using Edit > Add/Edit special sections: > Other Exolve sections.`, `You can specify up to a 100 desired words to fill, using the "Set preferred fills" button near the bottom. These words will be prioritized in autofill as well as in the suggested fills list.`, `In a cryptic clue, you can specify a part of the clue to be the definition part using Ctrl-D after selecting it. This part gets underlined when the solution is revealed.`, `The "Analysis" button shows useful information about the grid, the grid-fill, and the clues. You can use it to check for issues such as: grids that are not fully connected, consecutive unchecked cells, too many long clues, too many uncommon entries, etc.`, `When setting a crossword with ninas, enter the nina letters first and mark them using Edit > Mark grid cell: > Toggle nina ($). While you fill the rest of the grid, clearing lights will not erase any nina cells.`, `You can add a preamble to the crossword (e.g., some special instructions) using Edit > Add/Edit special sections: > Preamble.`, `Wordplay tabs such as "Charades" and "Anagrams" show candidate wordplays for the currently selected entry. However, you can edit the fodder text directly in the tab to experiment with alternatives.`, `You can use autofill to create pangrams, and even constrained pangrams, where all the letters in the alphabet get used over some specified cells, such as circled cells and unchecked cells.`, `You can create a 3-D crossword using Open > New 3-D grid:. You can also reverse some lights with Edit > Reverse current light. You can let autofill suggest reversals using the "Try reversals: [ ]" option on the main Exet tab. Reversed lights are often seen in 3-D crosswords.`, ]; this.tipIdx = -1; this.TIP_ENUM_MISMATCH = 0; this.TIP_ANALYSIS = 3; this.lastTipShownTime = 0; }; Exet.prototype.setMinPop = function(m) { if (m < 0) m = 0 this.minpop = m this.indexMinPop = Math.floor(exetLexicon.startLen * (100 - m) / 100) } Exet.prototype.startNav = function(dir='A', row=0, col=0) { if (!this.puz) return; if (row < 0 || row >= this.puz.gridHeight || col < 0 || col >= this.puz.gridWidth) { row = 0 col = 0 } if (dir != 'A' && dir != 'D' && dir != 'Z') { dir = 'A' let gridCell = this.puz.grid[row][col] if (gridCell.isLight && !gridCell.acrossClueLabel) { if (gridCell.downClueLabel) { dir = 'D' } else if (gridCell.z3dClueLabel) { dir = 'Z' } } } this.puz.currRow = row this.puz.currCol = col this.puz.currDir = dir if (this.puz.grid[row][col].isLight) { this.puz.activateCell(row, col) } else { this.navDarkness(row, col) } } Exet.prototype.hideExolveElement = function(suffix) { const elt = document.getElementById(this.puz.prefix + '-' + suffix); if (elt) { elt.style.display = 'none'; } } Exet.prototype.setPuzzle = function(puz) { if (puz.hasDgmlessCells) { alert('Diagramless cells are not supported'); return; } if (puz.hasNodirClues) { alert('Nodir clues not yet supported'); return; } if (puz.hasRebusCells) { alert('Rebus cells are not supported'); return; } if (puz.offNumClueIndices.length > 0) { alert('Non-numeric clues not yet supported'); return; } if ((puz.language && puz.language != exetLexicon.language) || (!puz.language && 'en' != exetLexicon.language) || (puz.languageScript && puz.languageScript != exetLexicon.script) || (!puz.languageScript && 'Latin' != exetLexicon.script)) { alert('The lexicon is in ' + exetLexicon.language + ' (' + exetLexicon.script + ') but the ' + 'puzzle has ' + puz.language + ' (' + puz.languageScript + ')'); return; } if (puz.langMaxCharCodes != exetLexicon.maxCharCodes) { alert('Lexicon has MaxCharCodes = ' + exetLexicon.maxCharCodes + ' but the puzzle has ' + puz.langMaxCharCodes); return; } if (puz.columnarLayout) { puz.columnarLayout = false; puz.gridcluesContainer.className = 'xlv-grid-and-clues-flex'; puz.cluesContainer.className = 'xlv-clues xlv-clues-flex'; } let gridFillChanges = false; for (let i = 0; i < puz.gridHeight; i++) { for (let j = 0; j < puz.gridWidth; j++) { const gridCell = puz.grid[i][j]; if (!gridCell.isLight) continue; if (gridCell.skipNum) { alert('Skipped-number cells not yet supported'); return; } if (gridCell.solution == '0') { gridCell.solution = '?'; gridFillChanges = true; } if (gridCell.solution != '?' && !exetLexicon.letterSet[gridCell.solution]) { alert('Entry ' + gridCell.solution + ' in grid[' + i + '][' + j + '] is not present in the lexicon'); return; } } } this.puz = puz; puz.useWebifi = false; puz.hltOverwrittenMillis = 0; puz.revealAll(false); if (!this.prefix && !this.suffix) { this.prefix = '' + '\n' + '\n' + '\n' + '\n' + '\n' + '\n' + '