UNPKG

@xwordly/xword-parser

Version:

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

231 lines (228 loc) 6.86 kB
'use strict'; var chunk3QROV6K6_js = require('./chunk-3QROV6K6.js'); var chunkKVCCVFYY_js = require('./chunk-KVCCVFYY.js'); // src/xd.ts function parseMetadata(lines) { const metadata = {}; for (const line of lines) { const colonIndex = line.indexOf(":"); if (colonIndex === -1) continue; const key = line.substring(0, colonIndex).trim(); const value = line.substring(colonIndex + 1).trim(); if (key && value) { const normalizedKey = (key[0] ?? "").toLowerCase() + key.slice(1); metadata[normalizedKey] = value; } } return metadata; } function parseGrid(lines) { return lines.map((line) => { const cells = []; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char !== void 0) { cells.push(char); } } return cells; }); } function parseClues(lines) { const across = []; const down = []; for (const line of lines) { if (line.length === 0) continue; const match = line.match(/^([AD])(\d+)\.\s+(.+?)(?:\s+~\s+(.+))?$/); if (match) { const [, direction, number, clue, answer] = match; const clueObj = { number: number ?? "", clue: clue?.trim() ?? "", answer: answer?.trim() ?? "" }; if (direction === "A") { across.push(clueObj); } else { down.push(clueObj); } } } return { across, down }; } function splitIntoSections(content) { const lines = content.split("\n"); const sections = []; let currentSection = []; let blankLineCount = 0; for (const line of lines) { if (line.trim() === "") { blankLineCount++; if (blankLineCount >= 2 && currentSection.length > 0) { sections.push(currentSection); currentSection = []; blankLineCount = 0; } } else { if (blankLineCount === 1 && currentSection.length > 0) { sections.push(currentSection); currentSection = []; } blankLineCount = 0; currentSection.push(line); } } if (currentSection.length > 0) { sections.push(currentSection); } const result = { metadata: [], grid: [], clues: [], notes: [] }; let sectionIndex = 0; if (sections.length > sectionIndex) { const section = sections[sectionIndex]; if (section && section.some((line) => line.includes(":"))) { result.metadata = section; sectionIndex++; } } if (sections.length > sectionIndex) { const section = sections[sectionIndex]; if (section && section.every((line) => /^[A-Za-z0-9#._]+$/.test(line) && line.length > 0)) { result.grid = section; sectionIndex++; } } while (sections.length > sectionIndex) { const section = sections[sectionIndex]; if (section && section.some((line) => /^[AD]\d+\./.test(line))) { result.clues.push(...section); sectionIndex++; } else { break; } } if (sections.length > sectionIndex) { for (let i = sectionIndex; i < sections.length; i++) { const section = sections[i]; if (section) { result.notes.push(...section); } } } return result; } function parseXd(content, options) { const sections = splitIntoSections(content); if (sections.grid.length === 0) { throw new chunkKVCCVFYY_js.XdParseError("Invalid XD file: no grid section found", "XD_FORMAT_ERROR" /* XD_FORMAT_ERROR */); } const metadata = parseMetadata(sections.metadata); const grid = parseGrid(sections.grid); const { across, down } = parseClues(sections.clues); if (grid.length === 0) { throw new chunkKVCCVFYY_js.XdParseError("Grid is empty", "XD_INVALID_GRID" /* XD_INVALID_GRID */); } const width = grid[0]?.length || 0; if (width === 0) { throw new chunkKVCCVFYY_js.XdParseError("Grid has no columns", "XD_INVALID_GRID" /* XD_INVALID_GRID */); } const height = grid.length; const maxWidth = options?.maxGridSize?.width ?? chunk3QROV6K6_js.MAX_GRID_WIDTH; const maxHeight = options?.maxGridSize?.height ?? chunk3QROV6K6_js.MAX_GRID_HEIGHT; if (width > maxWidth || height > maxHeight) { throw new chunkKVCCVFYY_js.XdParseError( `Grid dimensions too large: ${width}x${height}. Maximum supported size is ${maxWidth}x${maxHeight}`, "XD_INVALID_GRID" /* XD_INVALID_GRID */ ); } for (let i = 0; i < grid.length; i++) { if (!grid[i] || grid[i]?.length !== width) { throw new chunkKVCCVFYY_js.XdParseError( `Grid row ${i} has inconsistent width (expected ${width}, got ${grid[i]?.length || 0})`, "XD_INVALID_GRID" /* XD_INVALID_GRID */ ); } } const puzzle = { metadata, grid, across, down }; if (sections.notes.length > 0) { puzzle.notes = sections.notes.join("\n").trim(); } return puzzle; } function convertXdToUnified(puzzle) { const firstRow = puzzle.grid[0]; if (!firstRow) { throw new Error("Invalid state: grid is empty"); } const grid = { width: firstRow.length, height: puzzle.grid.length, cells: [] }; let cellNumber = 1; for (let y = 0; y < puzzle.grid.length; y++) { const row = puzzle.grid[y]; const cellRow = []; for (let x = 0; x < row.length; x++) { const cellValue = row[x]; const isBlack = cellValue === "#"; let number; if (!isBlack) { const needsNumber = ( // Start of across word (x === 0 || puzzle.grid[y][x - 1] === "#") && x < row.length - 1 && puzzle.grid[y][x + 1] !== "#" || // Start of down word (y === 0 || puzzle.grid[y - 1]?.[x] === "#") && y < puzzle.grid.length - 1 && puzzle.grid[y + 1]?.[x] !== "#" ); if (needsNumber) { number = cellNumber++; } } cellRow.push({ solution: isBlack ? void 0 : cellValue, number, isBlack }); } grid.cells.push(cellRow); } const clues = { across: puzzle.across.map((c) => ({ number: parseInt(c.number), text: c.clue })), down: puzzle.down.map((c) => ({ number: parseInt(c.number), text: c.clue })) }; const result = { title: puzzle.metadata.title, author: puzzle.metadata.author, copyright: puzzle.metadata.copyright, notes: puzzle.notes, date: puzzle.metadata.date, grid, clues }; const additionalProps = {}; if (puzzle.metadata.editor) additionalProps.editor = puzzle.metadata.editor; if (puzzle.metadata.rebus) additionalProps.rebus = puzzle.metadata.rebus; if (puzzle.metadata.notepad) additionalProps.notepad = puzzle.metadata.notepad; if (Object.keys(additionalProps).length > 0) { result.additionalProperties = additionalProps; } return result; } exports.convertXdToUnified = convertXdToUnified; exports.parseXd = parseXd; //# sourceMappingURL=chunk-RB5LJQIT.js.map //# sourceMappingURL=chunk-RB5LJQIT.js.map