UNPKG

@xwordly/xword-parser

Version:

Fast, type-safe TypeScript library for parsing crossword puzzles (PUZ, iPUZ, JPZ, XD)

320 lines (318 loc) 11.4 kB
import { MAX_GRID_WIDTH, MAX_GRID_HEIGHT } from './chunk-GKUILQIP.mjs'; import { JpzParseError, UnsupportedPuzzleTypeError } from './chunk-O732NFC7.mjs'; import { XMLParser } from 'fast-xml-parser'; function isJpzXmlNode(value) { return typeof value === "object" && value !== null; } function isString(value) { return typeof value === "string"; } function parseStringAttribute(value) { return isString(value) ? value : void 0; } function parseIntAttribute(value) { if (isString(value)) { const parsed = Number.parseInt(value, 10); return Number.isNaN(parsed) ? void 0 : parsed; } return void 0; } function parseMetadata(metadataNode) { if (!isJpzXmlNode(metadataNode)) return {}; return { title: parseStringAttribute(metadataNode.title), creator: parseStringAttribute(metadataNode.creator) || parseStringAttribute(metadataNode.author), copyright: parseStringAttribute(metadataNode.copyright), description: parseStringAttribute(metadataNode.description), publisher: parseStringAttribute(metadataNode.publisher), identifier: parseStringAttribute(metadataNode.identifier) }; } function parseCells(gridNode, options) { const cells = /* @__PURE__ */ new Map(); if (!isJpzXmlNode(gridNode)) { throw new JpzParseError("Missing or invalid grid element", "JPZ_MISSING_GRID" /* JPZ_MISSING_GRID */); } const width = parseIntAttribute(gridNode["@_width"]); const height = parseIntAttribute(gridNode["@_height"]); if (width === void 0 || height === void 0) { throw new JpzParseError( "Grid dimensions (width and height) are required", "JPZ_INVALID_GRID" /* JPZ_INVALID_GRID */ ); } const maxWidth = options?.maxGridSize?.width ?? MAX_GRID_WIDTH; const maxHeight = options?.maxGridSize?.height ?? MAX_GRID_HEIGHT; if (width <= 0 || width > maxWidth || height <= 0 || height > maxHeight) { throw new JpzParseError( `Invalid grid dimensions: ${width}x${height}. Maximum supported size is ${maxWidth}x${maxHeight}`, "JPZ_INVALID_GRID" /* JPZ_INVALID_GRID */, { details: { width, height } } ); } const cellNodes = gridNode.cell; if (cellNodes) { const cellArray = Array.isArray(cellNodes) ? cellNodes : [cellNodes]; for (const cellNode of cellArray) { if (!isJpzXmlNode(cellNode)) continue; const x = parseIntAttribute(cellNode["@_x"]); const y = parseIntAttribute(cellNode["@_y"]); if (x === void 0 || y === void 0) continue; const key = `${x},${y}`; const jpzCell = { x, y, type: parseStringAttribute(cellNode["@_type"]) === "block" ? "block" : "cell", solution: parseStringAttribute(cellNode["@_solution"]) || parseStringAttribute(cellNode["@_letter"]), number: parseIntAttribute(cellNode["@_number"]), isCircled: parseStringAttribute(cellNode["@_background-shape"]) === "circle", backgroundColor: parseStringAttribute(cellNode["@_background-color"]), barTop: parseStringAttribute(cellNode["@_top-bar"]) === "true", barBottom: parseStringAttribute(cellNode["@_bottom-bar"]) === "true", barLeft: parseStringAttribute(cellNode["@_left-bar"]) === "true", barRight: parseStringAttribute(cellNode["@_right-bar"]) === "true" }; cells.set(key, jpzCell); } } return { cells, width, height }; } function buildGrid(cells, width, height) { const grid = []; for (let y = 1; y <= height; y++) { const row = []; for (let x = 1; x <= width; x++) { const key = `${x},${y}`; const cell = cells.get(key); if (cell) { row.push(cell); } else { row.push({ x, y, type: "cell" }); } } grid.push(row); } return grid; } function parseClues(cluesNode) { const across = []; const down = []; if (!isJpzXmlNode(cluesNode)) { return { across, down }; } const clueArrays = Array.isArray(cluesNode) ? cluesNode : [cluesNode]; for (const clueSet of clueArrays) { if (!isJpzXmlNode(clueSet)) continue; let titleStr = ""; if (isString(clueSet.title)) { titleStr = clueSet.title; } else if (isJpzXmlNode(clueSet.title)) { const titleNode = clueSet.title; titleStr = parseStringAttribute(titleNode.b) || parseStringAttribute(titleNode["#text"]) || JSON.stringify(titleNode); } else if (parseStringAttribute(clueSet["@_title"])) { titleStr = parseStringAttribute(clueSet["@_title"]) || ""; } const isAcross = titleStr.toLowerCase().includes("across"); const targetArray = isAcross ? across : down; const clueNodes = clueSet.clue; if (clueNodes) { const clues = Array.isArray(clueNodes) ? clueNodes : [clueNodes]; for (const clue of clues) { if (isString(clue)) { const match = clue.match(/^(\d+)\.\s*(.+)$/); if (match) { targetArray.push({ number: match[1] || "", text: match[2] || "" }); } } else if (isJpzXmlNode(clue)) { const clueObj = { number: parseStringAttribute(clue["@_number"]) || parseStringAttribute(clue.number) || "", text: parseStringAttribute(clue["#text"]) || parseStringAttribute(clue.text) || parseStringAttribute(clue["@_text"]) || "", format: parseStringAttribute(clue["@_format"]) }; const wordRef = parseStringAttribute(clue["@_word"]); if (wordRef) { clueObj.number = wordRef; } targetArray.push(clueObj); } } } } return { across, down }; } function parseWords(wordsNode) { const words = []; if (!isJpzXmlNode(wordsNode) || !wordsNode.word) { return words; } const wordNodes = Array.isArray(wordsNode.word) ? wordsNode.word : [wordsNode.word]; for (const word of wordNodes) { if (!isJpzXmlNode(word)) continue; const jpzWord = { id: parseStringAttribute(word["@_id"]) || "", cells: [] }; if (isJpzXmlNode(word.cells)) { const cellList = word.cells.cell; if (cellList) { const cells = Array.isArray(cellList) ? cellList : [cellList]; for (const cell of cells) { if (!isJpzXmlNode(cell)) continue; const x = parseIntAttribute(cell["@_x"]); const y = parseIntAttribute(cell["@_y"]); if (x !== void 0 && y !== void 0) { jpzWord.cells.push({ x, y }); } } } } words.push(jpzWord); } return words; } function parseJpz(content, options) { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_", textNodeName: "#text", parseAttributeValue: false }); let doc; try { doc = parser.parse(content); } catch (e) { throw new JpzParseError( `Invalid XML: ${e instanceof Error ? e.message : "Unknown error"}`, "JPZ_INVALID_XML" /* JPZ_INVALID_XML */, void 0, e ); } const puzzleRoot = doc["crossword-compiler-applet"] || doc["crossword-compiler"] || doc.puzzle || doc.crossword; if (!isJpzXmlNode(puzzleRoot)) { throw new JpzParseError("no recognized root element", "JPZ_INVALID_XML" /* JPZ_INVALID_XML */); } let rectangularPuzzle = puzzleRoot["rectangular-puzzle"]; if (!isJpzXmlNode(rectangularPuzzle) && isJpzXmlNode(puzzleRoot.puzzle)) { const puzzleChild = puzzleRoot.puzzle; if (isJpzXmlNode(puzzleChild)) { rectangularPuzzle = puzzleChild["rectangular-puzzle"]; } } if (!isJpzXmlNode(rectangularPuzzle)) { rectangularPuzzle = puzzleRoot; } if (rectangularPuzzle.coded) { throw new UnsupportedPuzzleTypeError("Coded/cipher crosswords (Kaidoku)"); } if (rectangularPuzzle.sudoku || rectangularPuzzle.kakuro) { throw new UnsupportedPuzzleTypeError("Number puzzles"); } if (rectangularPuzzle["word-search"] || rectangularPuzzle.wordsearch) { throw new UnsupportedPuzzleTypeError("Word search"); } const metadata = parseMetadata(rectangularPuzzle.metadata); const crosswordNode = rectangularPuzzle.crossword || rectangularPuzzle.puzzle || rectangularPuzzle; let gridNode = void 0; if (isJpzXmlNode(crosswordNode)) { gridNode = crosswordNode.grid || crosswordNode.Grid; } if (!gridNode && isJpzXmlNode(rectangularPuzzle)) { gridNode = rectangularPuzzle.grid; } if (!gridNode) { throw new JpzParseError("no grid found", "JPZ_INVALID_XML" /* JPZ_INVALID_XML */); } const { cells, width, height } = parseCells(gridNode, options); const grid = buildGrid(cells, width, height); let cluesNode = void 0; if (isJpzXmlNode(crosswordNode)) { cluesNode = crosswordNode.clues || crosswordNode.Clues; } if (!cluesNode && isJpzXmlNode(rectangularPuzzle)) { cluesNode = rectangularPuzzle.clues || rectangularPuzzle.Clues; } const { across, down } = parseClues(cluesNode); let wordsNode = void 0; if (isJpzXmlNode(crosswordNode)) { wordsNode = crosswordNode.words || crosswordNode.Words; } const words = wordsNode ? parseWords(wordsNode) : void 0; return { width, height, metadata, grid, across, down, words }; } function convertJpzToUnified(puzzle) { const grid = { width: puzzle.width, height: puzzle.height, cells: [] }; for (const row of puzzle.grid) { const cellRow = []; for (const cell of row) { const unifiedCell = { solution: cell.solution, number: cell.number, isBlack: cell.type === "block", isCircled: cell.isCircled }; if (cell.backgroundColor || cell.barTop || cell.barBottom || cell.barLeft || cell.barRight) { unifiedCell.additionalProperties = {}; if (cell.backgroundColor) { unifiedCell.additionalProperties.backgroundColor = cell.backgroundColor; } if (cell.barTop) unifiedCell.additionalProperties.barTop = cell.barTop; if (cell.barBottom) unifiedCell.additionalProperties.barBottom = cell.barBottom; if (cell.barLeft) unifiedCell.additionalProperties.barLeft = cell.barLeft; if (cell.barRight) unifiedCell.additionalProperties.barRight = cell.barRight; } cellRow.push(unifiedCell); } grid.cells.push(cellRow); } const clues = { across: puzzle.across.map((c) => ({ number: typeof c.number === "string" ? parseInt(c.number) : c.number, text: c.text })), down: puzzle.down.map((c) => ({ number: typeof c.number === "string" ? parseInt(c.number) : c.number, text: c.text })) }; const result = { title: puzzle.metadata.title, author: puzzle.metadata.creator, copyright: puzzle.metadata.copyright, grid, clues }; const additionalProps = {}; if (puzzle.metadata.description) additionalProps.description = puzzle.metadata.description; if (puzzle.metadata.publisher) additionalProps.publisher = puzzle.metadata.publisher; if (puzzle.metadata.identifier) additionalProps.identifier = puzzle.metadata.identifier; if (puzzle.words && puzzle.words.length > 0) { additionalProps.words = puzzle.words; } if (Object.keys(additionalProps).length > 0) { result.additionalProperties = additionalProps; } return result; } export { convertJpzToUnified, parseJpz }; //# sourceMappingURL=chunk-KP7U2ZYG.mjs.map //# sourceMappingURL=chunk-KP7U2ZYG.mjs.map