/*
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: v1.03, December 26, 2025
*/
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 Exet() {
this.version = 'v1.03, December 26, 2025';
this.puz = null;
this.prefix = '';
this.suffix = '';
this.exolveOtherSec = '';
this.preflex = [];
this.preflexSet = {};
this.preflexHash = null;
this.preflexInUse = {};
this.unpreflex = [];
this.unpreflexSet = {};
this.unpreflexHash = null;
this.noProperNouns = false;
this.requireEnums = true;
this.noStemDupes = exetLexicon.hasOwnProperty('stems');
this.asymOK = false;
this.tryReversals = false;
this.lightRegexps = {};
this.lightRegexpsC = {};
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]},
}
/**
* Max lengths of preferred/disallowed word lists.
*/
this.MAX_PREFLEX = 50000;
// 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.throttledLightRegexpTimer = null;
this.inputLagMS = 400;
this.longInputLagMS = 2000;
this.sweepMS = 500;
// Params for light choices shown.
this.sweepMaxChoices = 5000;
this.sweepMaxChoicesSmall = 4;
this.shownLightChoices = 200;
/**
* Local storage usage. Filled by the first call to checkStorage().
*/
this.lsUsed = -1;
this.lsUsedAtStart = -1;
this.lsLeftAtStart = -1;
this.lsLeftIsAmple = true;
this.tipsList = [
`If you want to allow enum mismatches, then add the line
exolve-option: ignore-enum-mismatchusing Edit > Add/Edit special sections: > Other Exolve sections.`, `You can specify up to ${this.MAX_PREFLEX} 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.`, `When looking at any list of indicators through the "Lists" tab, you can type a topic word that describes the clue surface that you're trying to craft (in the blank provided near the top-right corner), and hit Enter, to highlight all words that might be related to that topic.`, `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.`, `Saving the crossword as HTML (Exolve format) with solutions included can be done using the keyboard shortcut Ctrl-s (Cmd-s on Mac). Exet overrides the browser's default "save" functionality.`, `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 specify a regular expression that constrains fill-choices shown for a light (for example, forcing a palindrome, or a specific substring) using the "Regexp constraint" option in the hamburger menu above the current clue.`, `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.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); } } Exet.prototype.setMinPop = function(m) { if (m < 0) { m = 0; } if (m > 100) { m = 100; } this.minpop = m; this.indexMinPop = Math.max( 1, 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' + '