UNPKG

xpuz

Version:

Parses and creates crossword puzzle files

959 lines (771 loc) 30.9 kB
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } /** * PUZ file parser. * * @module xpuz/parsers/puz * @see {@link module:xpuz/puzzle|Puzzle} */ var map = require("lodash/map"); var get = require("lodash/get"); var range = require("lodash/range"); var reverse = require("lodash/reverse"); var zip = require("lodash/zip"); var each = require("lodash/each"); var reduce = require("lodash/reduce"); var flatten = require("lodash/flatten"); var padStart = require("lodash/padStart"); var chunk = require("lodash/chunk"); var findKey = require("lodash/findKey"); var compact = require("lodash/compact"); var size = require("lodash/size"); var iconv = require("iconv-lite"); var PUZReader = require("./puz/puz-reader"); var Puzzle = require("../puzzle"); var BLOCK_CELL_VALUE = "."; var BLOCK_CELL_VALUE_REGEX = /\./g; var EXTENSION_HEADER_LENGTH = 8; var HEADER_CHECKSUM_BYTE_LENGTH = 8; var MAGIC_CHECKSUM_BYTE_LENGTH = 8; var UNKNOWN1_BYTE_LENGTH = 2; var UNKNOWN2_BYTE_LENGTH = 12; var CHECKSUM_BUFFER_LENGTH = 2; var NUMBER_OF_CLUES_BUFFER_LENGTH = 2; var PUZZLE_TYPE_BUFFER_LENGTH = 2; var SOLUTION_STATE_BUFFER_LENGTH = 2; var HEADER_BUFFER_LENGTH = 52; var EXTENSION_LENGTH_BUFFER_LENGTH = 2; var EXTENSION_NAME_LENGTH = 4; var PUZZLE_KEY_LENGTH = 4; var RTBL_KEY_PADDING_WIDTH = 2; var PUZZLE_TYPE = { Normal: 0x0001, Diagramless: 0x0401 }; var SOLUTION_STATE = { // solution is available in plaintext Unlocked: 0x0000, // solution is locked (scrambled) with a key Locked: 0x0004 }; var CELL_STATES = { PreviouslyIncorrect: 0x10, CurrentlyIncorrect: 0x20, AnswerGiven: 0x40, Circled: 0x80 }; var ATOZ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; var MINIMUM_KEY_VALUE = 1000; var MAXIMUM_KEY_VALUE = 9999; function _doChecksum(buffer, cksum) { for (var i = 0; i < buffer.length; i++) { // right-shift one with wrap-around var lowbit = cksum & 0x0001; cksum = cksum >> 1; if (lowbit) { // eslint-disable-next-line no-magic-numbers cksum = cksum | 0x8000; } // then add in the data and clear any carried bit past 16 // eslint-disable-next-line no-magic-numbers cksum = cksum + buffer.readUInt8(i) & 0xFFFF; } return cksum; } function _readHeader(reader, options) { var data = {}; data.globalChecksum = reader._readUInt16(); reader._seek("ACROSS&DOWN\0".length, { current: true }); data.headerChecksum = reader._readUInt16(); data.magicChecksum = reader._readValues(MAGIC_CHECKSUM_BYTE_LENGTH); data.version = reader._readString(); data.unknown1 = reader._readValues(UNKNOWN1_BYTE_LENGTH); data.scrambledChecksum = reader._readUInt16(); data.unknown2 = reader._readValues(UNKNOWN2_BYTE_LENGTH); data.width = reader._readUInt8(); data.height = reader._readUInt8(); data.numberOfClues = reader._readUInt16(); data.puzzleType = reader._readUInt16(); data.solutionState = reader._readUInt16(); if (data.solutionState === SOLUTION_STATE.Locked && !options.solutionKey) { throw new Error("Puzzle solution is locked and no solutionKey option was provided"); } return data; } function _processExtension(extension) { if (extension.name === "GRBS") { extension.board = map(extension.data, function (b) { if (b === 0) { return null; } return b - 1; }); } if (extension.name === "RTBL") { extension.rebus_solutions = reduce(iconv.decode(extension.data, PUZReader.ENCODING).split(";"), function (solutions, solutionPair) { var pair = solutionPair.split(":"); pair[0] = parseInt(pair[0], 10); solutions[pair[0]] = pair[1]; return solutions; }, {}); } if (extension.name === "LTIM") { var timings = iconv.decode(extension.data, PUZReader.ENCODING).split(","); extension.timing = { elapsed: parseInt(timings[0], 10), running: timings[1] === "0" }; } if (extension.name === "GEXT") { extension.cell_states = map(extension.data, function (b) { return { PreviouslyIncorrect: !!(b & CELL_STATES.PreviouslyIncorrect), CurrentlyIncorrect: !!(b & CELL_STATES.CurrentlyIncorrect), AnswerGiven: !!(b & CELL_STATES.AnswerGiven), Circled: !!(b & CELL_STATES.Circled) }; }); } if (extension.name === "RUSR") { extension.user_rebus_entries = map(iconv.decode(extension.data, PUZReader.ENCODING).split("\0"), function (entry) { return entry === "" ? null : entry; }); } return extension; } function _readExtension(reader) { var extension = {}; extension.name = reader._readString(EXTENSION_NAME_LENGTH); var length = reader._readUInt16(); extension.checksum = reader._readUInt16(); // Include null byte at end extension.data = reader._readValues(length + 1); // Remove null byte at the end extension.data = extension.data.slice(0, -1); return _processExtension(extension); } function _parseEnd(reader, data) { var remainingLength = reader.size() - reader.tell(); if (remainingLength >= EXTENSION_HEADER_LENGTH) { var extension = _readExtension(reader); data.extensions = data.extensions || {}; data.extensions[extension.name] = extension; delete extension.name; _parseEnd(reader, data); } } function _parseExtensions(reader, puzzleData) { var data = {}; _parseEnd(reader, data); if (get(data, "extensions.GRBS")) { each(flatten(puzzleData.grid), function (cell, index) { var c = cell; if (data.extensions.GRBS.board[index] === null) { return; } var rebusSolution = data.extensions.RTBL.rebus_solutions[data.extensions.GRBS.board[index]]; c.solution = rebusSolution; }); } if (get(data, "extensions.RUSR")) { data.extensions.RUSR.user_rebus_entries.forEach(function (rusr, index) { if (rusr !== null) { var y = Math.floor(index / puzzleData.header.width); var x = index % puzzleData.header.width; puzzleData.solution[y][x] = rusr; } }); } puzzleData._extensions = data.extensions; puzzleData.timing = get(data, "extensions.LTIM.timing"); } function _readClues(reader, numberOfClues) { var clues = []; for (var i = 0; i < numberOfClues; i++) { clues.push(reader._readString()); } return clues; } function _generateGridAndClues(answers, clueList) { function _isBlockCell(x, y) { return answers[y][x] === BLOCK_CELL_VALUE; } var clues = { across: {}, down: {} }; var grid = []; var width = answers[0].length, height = answers.length; var clueCount = 0; var clueListIndex = 0; for (var y = 0; y < height; y++) { var row = []; for (var x = 0; x < width; x++) { var cell = {}; if (_isBlockCell(x, y)) { cell.isBlockCell = true; } else { cell.solution = answers[y][x]; var down = false, across = false; if ((x === 0 || _isBlockCell(x - 1, y)) && x + 1 < width && !_isBlockCell(x + 1, y)) { across = true; } if ((y === 0 || _isBlockCell(x, y - 1)) && y + 1 < height && !_isBlockCell(x, y + 1)) { down = true; } if (across || down) { cell.clueNumber = ++clueCount; } if (across) { clues.across[clueCount] = clueList[clueListIndex++]; } if (down) { clues.down[clueCount] = clueList[clueListIndex++]; } } row.push(cell); } grid.push(row); } return { grid: grid, clues: clues }; } function _pluckSolutions(grid) { return grid.map(function (row) { return row.map(function (cell) { if (cell.isBlockCell) { return BLOCK_CELL_VALUE; } if (cell.solution === null) { return " "; } return cell.solution; }); }); } function _flattenSolution(solution) { return flatten(solution).map(function (entry) { if (entry === null) { return BLOCK_CELL_VALUE; } if (entry === "") { return "-"; } return entry[0]; }).join(""); } function _unflattenSolution(solution, width) { return chunk(solution.split(""), width).map(function (row) { return row.map(function (cell) { return cell === "-" ? "" : cell; }); }); } function _textChecksum(puzzleData, checksum) { if (puzzleData.title) { checksum = _doChecksum(iconv.encode(puzzleData.title + "\0", PUZReader.ENCODING), checksum); } if (puzzleData.author) { checksum = _doChecksum(iconv.encode(puzzleData.author + "\0", PUZReader.ENCODING), checksum); } if (puzzleData.copyright) { checksum = _doChecksum(iconv.encode(puzzleData.copyright + "\0", PUZReader.ENCODING), checksum); } puzzleData.clueList.forEach(function (clue) { if (clue) { checksum = _doChecksum(iconv.encode(clue, PUZReader.ENCODING), checksum); } }); var versionParts = puzzleData.header.version.split(".").map(Number); // Notes only became part of the checksum starting in version 1.3 // (see https://github.com/alexdej/puzpy/blob/6109ad5a54359262010d01f2e0175d928bd70962/puz.py#L360) if (versionParts[0] >= 1 && versionParts[1] >= 3) { // eslint-disable-line no-magic-numbers if (puzzleData.notes) { checksum = _doChecksum(iconv.encode(puzzleData.notes + "\0", PUZReader.ENCODING), checksum); } } return checksum; } function _headerChecksum(puzzleData, checksum) { if (checksum === void 0) { checksum = 0; } var buffer = new Buffer(HEADER_CHECKSUM_BYTE_LENGTH); buffer.writeUInt8(puzzleData.header.width, 0); buffer.writeUInt8(puzzleData.header.height, 1); // These "magic numbers" are the successive byte offsets to write at /* eslint-disable no-magic-numbers */ buffer.writeUInt16LE(puzzleData.header.numberOfClues, 2); buffer.writeUInt16LE(puzzleData.header.puzzleType, 4); buffer.writeUInt16LE(puzzleData.header.solutionState, 6); /* eslint-enable no-magic-numbers */ return _doChecksum(buffer, checksum); } function _globalChecksum(puzzleData, headerChecksum) { var checksum = headerChecksum === void 0 ? _headerChecksum(puzzleData) : headerChecksum; var buffer = iconv.encode(puzzleData.answer, PUZReader.ENCODING); checksum = _doChecksum(buffer, checksum); buffer = iconv.encode(puzzleData.solution, PUZReader.ENCODING); checksum = _doChecksum(buffer, checksum); checksum = _textChecksum(puzzleData, checksum); return checksum; } function _magicChecksum(puzzleData) { var headerChecksum = _headerChecksum(puzzleData); var answerChecksum = _doChecksum(iconv.encode(puzzleData.answer, PUZReader.ENCODING)); var solutionChecksum = _doChecksum(iconv.encode(puzzleData.solution, PUZReader.ENCODING)); var textChecksum = _textChecksum(puzzleData); var MAGIC_CHECKSUM_STRING = "ICHEATED"; var magicChecksum = new Buffer([ /* eslint-disable no-magic-numbers */ MAGIC_CHECKSUM_STRING.charCodeAt(0) ^ headerChecksum & 0xFF, MAGIC_CHECKSUM_STRING.charCodeAt(1) ^ answerChecksum & 0xFF, MAGIC_CHECKSUM_STRING.charCodeAt(2) ^ solutionChecksum & 0xFF, MAGIC_CHECKSUM_STRING.charCodeAt(3) ^ textChecksum & 0xFF, MAGIC_CHECKSUM_STRING.charCodeAt(4) ^ (headerChecksum & 0xFF00) >> 8, MAGIC_CHECKSUM_STRING.charCodeAt(5) ^ (answerChecksum & 0xFF00) >> 8, MAGIC_CHECKSUM_STRING.charCodeAt(6) ^ (solutionChecksum & 0xFF00) >> 8, MAGIC_CHECKSUM_STRING.charCodeAt(7) ^ (textChecksum & 0xFF00) >> 8 /* eslint-enable no-magic-numbers */ ]); return magicChecksum; } function _transposeGrid(gridString, width, height) { var data = gridString.match(new RegExp(".{1," + width + "}", "g")); return range(width).map(function (c) { return range(height).map(function (r) { return data[r][c]; }).join(""); }).join(""); } function _restoreSolution(s, t) { /* s is the source string, it can contain '.' t is the target, it's smaller than s by the number of '.'s in s Each char in s is replaced by the corresponding char in t, jumping over '.'s in s. >>> restore('ABC.DEF', 'XYZABC') 'XYZ.ABC' */ t = t.split(""); return s.split("").reduce(function (arr, c) { if (c === BLOCK_CELL_VALUE) { arr.push(c); } else { arr.push(t.shift()); } return arr; }, []).join(""); } function _shift(str, key) { return str.split("").map(function (c, index) { var letterIndex = (ATOZ.indexOf(c) + Number(key[index % key.length])) % ATOZ.length; if (letterIndex < 0) { letterIndex = ATOZ.length + letterIndex; } return ATOZ[letterIndex]; }).join(""); } function _unshift(str, key) { return _shift(str, map(key, function (k) { return -k; })); } function _everyOther(str) { return str.split("").reduce(function (arr, c, i) { // eslint-disable-next-line no-magic-numbers if (i % 2 === 0) { arr.push(c); } return arr; }, []).join(""); } function _unshuffle(str) { return _everyOther(str.substring(1)) + _everyOther(str); } function _unscrambleString(str, key) { var len = str.length; reverse(padStart(key, PUZZLE_KEY_LENGTH, "0").split("")).forEach(function (k) { str = _unshuffle(str); str = str.substring(len - k) + str.substring(0, len - k); str = _unshift(str, key); }); return str; } function _shuffle(str) { // eslint-disable-next-line no-magic-numbers var mid = Math.floor(str.length / 2); return zip(str.substring(mid).split(""), str.substring(0, mid).split("")).reduce(function (arr, chars) { if (chars[0] === void 0 || chars[1] === void 0) { return arr; } arr.push(chars[0] + chars[1]); return arr; }, [] // eslint-disable-next-line no-magic-numbers ).join("") + (str.length % 2 ? str[str.length - 1] : ""); } function _scrambleString(str, key) { /* str is the puzzle's solution in column-major order, omitting black squares: i.e. if the puzzle is: C A T # # A # # R solution is CATAR Key is a 4-digit number in the range 1000 <= key <= 9999 */ each(padStart(key, PUZZLE_KEY_LENGTH, "0"), function (k) { str = _shift(str, key); str = str.substring(k) + str.substring(0, k); str = _shuffle(str); }); return str; } function _scrambledChecksum(answer, width, height) { var transposed = _transposeGrid(_flattenSolution(answer), width, height).replace(BLOCK_CELL_VALUE_REGEX, ""); return _doChecksum(iconv.encode(transposed, PUZReader.ENCODING)); } function _validateChecksums(puzzleData) { var headerChecksum = _headerChecksum(puzzleData); var globalChecksum = _globalChecksum(puzzleData, headerChecksum); var magicChecksum = _magicChecksum(puzzleData); var checksums = { header: headerChecksum, global: globalChecksum, magic: magicChecksum }; var errors = []; if (checksums.header !== puzzleData.header.headerChecksum) { errors.push("header checksums do not match"); } if (checksums.global !== puzzleData.header.globalChecksum) { errors.push("global checksums do not match"); } if (!checksums.magic.equals(puzzleData.header.magicChecksum)) { errors.push("magic checksums do not match"); } each(puzzleData._extensions, function (extension, name) { if (extension.checksum !== _doChecksum(extension.data)) { errors.push("checksum for extension ".concat(name, " does not match")); } }); return errors; } function _scrambleSolution(solutionGrid, key) { var height = solutionGrid.length; var width = solutionGrid[0].length; var solutionString = flatten(_flattenSolution(solutionGrid)).join(""); var transposed = _transposeGrid(solutionString, width, height); var data = _restoreSolution(transposed, _scrambleString(transposed.replace(BLOCK_CELL_VALUE_REGEX, ""), key)); solutionString = _transposeGrid(data, height, width); return chunk(solutionString.split(""), width); } function _unscrambleSolution(puzzleData, key) { var transposed = _transposeGrid(puzzleData.answer, puzzleData.header.width, puzzleData.header.height); var data = _restoreSolution(transposed, _unscrambleString(transposed.replace(BLOCK_CELL_VALUE_REGEX, ""), key)); var result = _transposeGrid(data, puzzleData.header.height, puzzleData.header.width); if (result === puzzleData.answer) { throw new Error("Unscrambled solution is the same as the scrambled solution; incorrect key?"); } return result; } function _writeHeader(puzzleData, options) { var globalChecksumBuffer = new Buffer(CHECKSUM_BUFFER_LENGTH); globalChecksumBuffer.writeUInt16LE(_globalChecksum(puzzleData)); var headerChecksumBuffer = new Buffer(CHECKSUM_BUFFER_LENGTH); headerChecksumBuffer.writeUInt16LE(_headerChecksum(puzzleData)); var magicChecksumBuffer = _magicChecksum(puzzleData); var scrambledChecksumBuffer = new Buffer(CHECKSUM_BUFFER_LENGTH); if (get(options, "scrambled")) { scrambledChecksumBuffer.writeUInt16LE(_scrambledChecksum(puzzleData.unscrambledAnswer, puzzleData.header.width, puzzleData.header.height)); } else { scrambledChecksumBuffer.fill(0x0); } var numberOfCluesBuffer = new Buffer(NUMBER_OF_CLUES_BUFFER_LENGTH); numberOfCluesBuffer.writeUInt16LE(puzzleData.header.numberOfClues); var puzzleTypeBuffer = new Buffer(PUZZLE_TYPE_BUFFER_LENGTH); puzzleTypeBuffer.writeUInt16LE(puzzleData.header.puzzleType); var solutionStateBuffer = new Buffer(SOLUTION_STATE_BUFFER_LENGTH); solutionStateBuffer.writeUInt16LE(puzzleData.header.solutionState); return Buffer.concat([globalChecksumBuffer, iconv.encode("ACROSS&DOWN\0", PUZReader.ENCODING), headerChecksumBuffer, magicChecksumBuffer, iconv.encode(get(options, "version", puzzleData.header.version) + "\0", PUZReader.ENCODING), // unknown block 1 new Buffer([0x0, 0x0]), scrambledChecksumBuffer, // unknown block 2 new Buffer(UNKNOWN2_BYTE_LENGTH).fill(0x0), new Buffer([puzzleData.header.width]), new Buffer([puzzleData.header.height]), numberOfCluesBuffer, puzzleTypeBuffer, solutionStateBuffer], HEADER_BUFFER_LENGTH); } function _writeExtension(extensionBuffer, extensionName) { var lengthBuffer = new Buffer(EXTENSION_LENGTH_BUFFER_LENGTH); lengthBuffer.writeUInt16LE(extensionBuffer.length); var checksumBuffer = new Buffer(CHECKSUM_BUFFER_LENGTH); checksumBuffer.writeUInt16LE(_doChecksum(extensionBuffer)); return Buffer.concat([iconv.encode(extensionName, PUZReader.ENCODING), lengthBuffer, checksumBuffer, extensionBuffer, new Buffer([0])], EXTENSION_NAME_LENGTH + EXTENSION_LENGTH_BUFFER_LENGTH + CHECKSUM_BUFFER_LENGTH + extensionBuffer.length + 1); } function _writeGRBS(answerArray, rebusSolutions) { var grbsBuffer = new Buffer(answerArray.map(function (cell, index) { var solutionKey = findKey(rebusSolutions, function (solutionInfo) { return solutionInfo.cells.includes(index); }); if (solutionKey === void 0) { return 0; } return parseInt(solutionKey, 10) + 1; })); return _writeExtension(grbsBuffer, "GRBS"); } function _writeRTBL(rebusSolutions) { var rtblBuffer = iconv.encode(Object.keys(rebusSolutions).map(function (key) { return "".concat(padStart(key, RTBL_KEY_PADDING_WIDTH, " "), ":").concat(rebusSolutions[key].solution, ";"); }).join(""), PUZReader.ENCODING); return _writeExtension(rtblBuffer, "RTBL"); } function _writeRUSR(userSolutionArray) { var rusrBuffer = iconv.encode(userSolutionArray.map(function (solution) { if (solution.length > 1) { return "".concat(solution, "\0"); } return "\0"; }).join(""), PUZReader.ENCODING); return _writeExtension(rusrBuffer, "RUSR"); } function _writeLTIM(timing) { return _writeExtension(iconv.encode("".concat(timing.elapsed, ",").concat(timing.running ? "1" : "0"), PUZReader.ENCODING), "LTIM"); } function _writeRebus(answerArray, userSolutionArray, extensions) { var solutionKey = 0; var rebusSolutions = flatten(answerArray).reduce(function (solutions, cellSolution, cellIndex) { if (cellSolution && cellSolution.length > 1) { var key = findKey(solutions, { solution: cellSolution }); if (key === void 0) { solutions[++solutionKey] = { solution: cellSolution, cells: [cellIndex] }; } else { solutions[key].cells.push(cellIndex); } } return solutions; }, {}); var grbsBuffer = _writeGRBS(answerArray, rebusSolutions); var rtblBuffer = _writeRTBL(rebusSolutions); var rusrBuffer = _writeRUSR(userSolutionArray); var buffers = [grbsBuffer, rtblBuffer, rusrBuffer]; var totalBufferLength = grbsBuffer.length + rtblBuffer.length + rusrBuffer.length; if (extensions.timing) { var ltimBuffer = _writeLTIM(extensions.timing); buffers.push(ltimBuffer); totalBufferLength += ltimBuffer.length; } return Buffer.concat(buffers, totalBufferLength); } function _parsePuzzle(path, options) { var data = {}; var reader = new PUZReader(path); data.header = _readHeader(reader, options); var numberOfCells = data.header.width * data.header.height; data.answer = reader._readString(numberOfCells); if (data.header.solutionState === SOLUTION_STATE.Locked) { data.unscrambledAnswer = _unscrambleSolution({ header: data.header, answer: data.answer }, options.solutionKey); } else { data.unscrambledAnswer = data.answer; } data.solution = reader._readString(numberOfCells); data.title = reader._readString(); data.author = reader._readString(); data.copyright = reader._readString(); data.clueList = _readClues(reader, data.header.numberOfClues); var gridAndClues = _generateGridAndClues(_unflattenSolution(data.unscrambledAnswer, data.header.width), data.clueList); data.grid = gridAndClues.grid; data.clues = gridAndClues.clues; data.notes = reader._readString(); _parseExtensions(reader, data); return data; } function validatePuzzle(puzzle) { var checksumResults = _validateChecksums(puzzle); var errors = []; if (checksumResults) { errors.push.apply(errors, _toConsumableArray(checksumResults)); } return errors.length === 0 ? void 0 : errors; } function _getPuzzleData(path, options) { return new Promise(function (resolve, reject) { try { var puzzleData = _parsePuzzle(path, options); var errors = validatePuzzle(puzzleData); if (errors !== void 0) { reject("Invalid puzzle:\n\t".concat(errors.join("\n\t"))); } else { resolve({ info: { title: puzzleData.title || void 0, author: puzzleData.author || void 0, copyright: puzzleData.copyright || void 0, intro: puzzleData.notes || void 0, formatExtra: { version: puzzleData.header.version } }, grid: puzzleData.grid, clues: puzzleData.clues, userSolution: _unflattenSolution(puzzleData.solution, puzzleData.header.width), extensions: { timing: puzzleData.timing } }); } } catch (err) { reject(err); } }); } /** * Parser class for PUZ-formatted puzzles. * * @constructor */ var PUZParser = /*#__PURE__*/ function () { function PUZParser() { _classCallCheck(this, PUZParser); } _createClass(PUZParser, [{ key: "parse", /** * Parses a file in .puz format into a {@link module:xpuz/puzzle~Puzzle|Puzzle} object. * * @memberOf module:xpuz/parsers/puz~PUZParser * @function * @instance * * @param {string|external:Buffer|ArrayBuffer} path - the .puz file to parse, either as a file path * (strong) or a {@link external:Buffer|Buffer} or {@link external:ArrayBuffer|ArrayBuffer} containing the puzzle * content. * @param {object} [options] - an object of options to affect the parsing * @param {Number} [options.solutionKey] - an integer between 1000 and 9999, inclusive, to use to unlock * the puzzle's solution if the solution is locked. If the solution is not locked, this is ignored. * * @throws if the puzzle is locked and an invalid (or no) `options.solutionKey` was provided * * @returns {external:Bluebird} a promise that resolves with the {@link module:xpuz/puzzle~Puzzle|Puzzle} object */ value: function parse(path, options) { options = options || {}; return _getPuzzleData(path, options).then(function (puzzleData) { return new Puzzle(puzzleData); }); } /** * Given a {@link module:xpuz/puzzle~Puzzle|Puzzle} object, returns a {@link external:Buffer|Buffer} * containing the puzzle in .puz format. * * @memberOf module:xpuz/parsers/puz~PUZParser * @function * @instance * * @param {module:xpuz/puzzle~Puzzle} puzzle - the puzzle to convert to .puz content. * @param {object} [options] - an object containing additional options for the conversion * @param {boolean} [options.scrambled] - if true, the puzzle's solution will be scrambled * @param {Number} [options.solutionKey] - the solution key with which to scramble the solution. * If `options.scrambled` is true, this is required. * * @throws if `options.scrambled` is true but `options.solutionKey` is not a 4-digit integer * (between 1000 and 9999, inclusive). * * @returns {external:Buffer} a Buffer containing the .puz content. */ }, { key: "generate", value: function generate(puzzle, options) { puzzle = puzzle.toJSON(); var numberOfClues = size(puzzle.clues.across) + size(puzzle.clues.down); var puzzleType = PUZZLE_TYPE.Normal; var solutionState = SOLUTION_STATE.Unlocked; options = options || {}; var height = puzzle.grid.length; var width = puzzle.grid[0].length; var notes = puzzle.info.intro || ""; var answerArray = _pluckSolutions(puzzle.grid); var unscrambledAnswerArray; if (options.scrambled) { if (!options.solutionKey || Number(options.solutionKey) < MINIMUM_KEY_VALUE || Number(options.solutionKey) > MAXIMUM_KEY_VALUE) { throw new Error("Must specify a solution key that is an integer >= 1000 and <= 9999; was ".concat(options.solutionKey)); } unscrambledAnswerArray = answerArray; answerArray = _scrambleSolution(answerArray, options.solutionKey); solutionState = SOLUTION_STATE.Locked; } var flattenedAnswerArray = flatten(answerArray); var flattenedUnscrambledAnswerArray = flatten(unscrambledAnswerArray || answerArray); var userSolution = puzzle.userSolution.map(function (row) { return row.map(function (solution) { if (solution === null) { return BLOCK_CELL_VALUE; } if (solution === "") { return "-"; } return solution; }); }); var userSolutionArray = flatten(userSolution); var clueList = compact(flatten(puzzle.grid).map(function (cell) { return cell.clueNumber; })).reduce(function (cluesArray, clueNumber) { if (puzzle.clues.across[clueNumber] !== void 0) { cluesArray.push(puzzle.clues.across[clueNumber]); } if (puzzle.clues.down[clueNumber] !== void 0) { cluesArray.push(puzzle.clues.down[clueNumber]); } return cluesArray; }, []); var puzzleData = { header: { width: width, height: height, numberOfClues: numberOfClues, puzzleType: puzzleType, solutionState: solutionState, version: get(puzzle.info, "formatExtra.version") || "1.3" }, answer: _flattenSolution(flattenedAnswerArray), unscrambledAnswer: _flattenSolution(flattenedUnscrambledAnswerArray), solution: _flattenSolution(userSolution), title: puzzle.info.title, author: puzzle.info.author, copyright: puzzle.info.copyright, clueList: clueList, notes: notes }; var headerBuffer = _writeHeader(puzzleData, options); var answerStringBuffer = iconv.encode(_flattenSolution(answerArray), PUZReader.ENCODING); var userSolutionStringBuffer = iconv.encode(userSolutionArray.map(function (solution) { return solution[0]; }).join(""), PUZReader.ENCODING); var titleStringBuffer = iconv.encode("".concat(puzzle.info.title || "", "\0"), PUZReader.ENCODING); var authorStringBuffer = iconv.encode("".concat(puzzle.info.author || "", "\0"), PUZReader.ENCODING); var copyrightStringBuffer = iconv.encode("".concat(puzzle.info.copyright || "", "\0"), PUZReader.ENCODING); var cluesStringBuffer = iconv.encode("".concat(clueList.join("\0"), "\0"), PUZReader.ENCODING); var notesStringBuffer = iconv.encode("".concat(notes, "\0"), PUZReader.ENCODING); var buffers = [headerBuffer, answerStringBuffer, userSolutionStringBuffer, titleStringBuffer, authorStringBuffer, copyrightStringBuffer, cluesStringBuffer, notesStringBuffer]; var totalBufferLength = headerBuffer.length + answerStringBuffer.length + userSolutionStringBuffer.length + titleStringBuffer.length + authorStringBuffer.length + copyrightStringBuffer.length + cluesStringBuffer.length + notesStringBuffer.length; if (flattenedUnscrambledAnswerArray.some(function (solution) { return solution.length > 1; })) { var rebusBuffer = _writeRebus(flattenedUnscrambledAnswerArray, userSolutionArray, puzzle.extensions || {}); buffers.push(rebusBuffer); totalBufferLength += rebusBuffer.length; } return Buffer.concat(buffers, totalBufferLength); } }]); return PUZParser; }(); exports = module.exports = PUZParser; //# sourceMappingURL=puz.js.map