@xwordly/xword-parser
Version:
Fast, type-safe TypeScript library for parsing crossword puzzles (PUZ, iPUZ, JPZ, XD)
1 lines • 17.2 kB
Source Map (JSON)
{"version":3,"sources":["../src/xd.ts"],"names":[],"mappings":"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","file":"chunk-TVGBJSWO.mjs","sourcesContent":["import { XdParseError } from './errors';\nimport { ErrorCode } from './types';\nimport type { Puzzle, Grid, Cell as UnifiedCell, Clues, ParseOptions } from './types';\nimport { MAX_GRID_WIDTH, MAX_GRID_HEIGHT } from './constants';\n\nexport interface XdMetadata {\n title?: string;\n author?: string;\n editor?: string;\n copyright?: string;\n date?: string;\n rebus?: string;\n [key: string]: string | undefined;\n}\n\nexport interface XdClue {\n number: string;\n clue: string;\n answer: string;\n metadata?: Record<string, string>;\n}\n\nexport interface XdPuzzle {\n metadata: XdMetadata;\n grid: string[][];\n across: XdClue[];\n down: XdClue[];\n notes?: string;\n}\n\nfunction parseMetadata(lines: string[]): XdMetadata {\n const metadata: XdMetadata = {};\n\n for (const line of lines) {\n const colonIndex = line.indexOf(':');\n if (colonIndex === -1) continue;\n\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n\n if (key && value) {\n // Convert first character to lowercase to match TypeScript conventions\n const normalizedKey = (key[0] ?? '').toLowerCase() + key.slice(1);\n metadata[normalizedKey] = value;\n }\n }\n\n return metadata;\n}\n\nfunction parseGrid(lines: string[]): string[][] {\n return lines.map((line) => {\n const cells: string[] = [];\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n if (char !== undefined) {\n cells.push(char);\n }\n }\n return cells;\n });\n}\n\nfunction parseClues(lines: string[]): { across: XdClue[]; down: XdClue[] } {\n const across: XdClue[] = [];\n const down: XdClue[] = [];\n\n for (const line of lines) {\n if (line.length === 0) continue;\n\n const match = line.match(/^([AD])(\\d+)\\.\\s+(.+?)(?:\\s+~\\s+(.+))?$/);\n if (match) {\n const [, direction, number, clue, answer] = match;\n\n const clueObj: XdClue = {\n number: number ?? '',\n clue: clue?.trim() ?? '',\n answer: answer?.trim() ?? '',\n };\n\n if (direction === 'A') {\n across.push(clueObj);\n } else {\n down.push(clueObj);\n }\n }\n }\n\n return { across, down };\n}\n\nfunction splitIntoSections(content: string): {\n metadata: string[];\n grid: string[];\n clues: string[];\n notes: string[];\n} {\n const lines = content.split('\\n');\n const sections: string[][] = [];\n let currentSection: string[] = [];\n let blankLineCount = 0;\n\n for (const line of lines) {\n if (line.trim() === '') {\n blankLineCount++;\n if (blankLineCount >= 2 && currentSection.length > 0) {\n sections.push(currentSection);\n currentSection = [];\n blankLineCount = 0;\n }\n } else {\n if (blankLineCount === 1 && currentSection.length > 0) {\n sections.push(currentSection);\n currentSection = [];\n }\n blankLineCount = 0;\n currentSection.push(line);\n }\n }\n\n if (currentSection.length > 0) {\n sections.push(currentSection);\n }\n\n const result = {\n metadata: [] as string[],\n grid: [] as string[],\n clues: [] as string[],\n notes: [] as string[],\n };\n\n let sectionIndex = 0;\n\n if (sections.length > sectionIndex) {\n const section = sections[sectionIndex];\n if (section && section.some((line) => line.includes(':'))) {\n result.metadata = section;\n sectionIndex++;\n }\n }\n\n if (sections.length > sectionIndex) {\n const section = sections[sectionIndex];\n if (section && section.every((line) => /^[A-Za-z0-9#._]+$/.test(line) && line.length > 0)) {\n result.grid = section;\n sectionIndex++;\n }\n }\n\n while (sections.length > sectionIndex) {\n const section = sections[sectionIndex];\n if (section && section.some((line) => /^[AD]\\d+\\./.test(line))) {\n result.clues.push(...section);\n sectionIndex++;\n } else {\n break;\n }\n }\n\n if (sections.length > sectionIndex) {\n for (let i = sectionIndex; i < sections.length; i++) {\n const section = sections[i];\n if (section) {\n result.notes.push(...section);\n }\n }\n }\n\n return result;\n}\n\n/**\n * Parse an XD format crossword puzzle.\n *\n * @param content - The XD text data as string\n * @param options - Optional parsing options\n * @param options.maxGridSize - Maximum allowed grid dimensions\n * @returns An XdPuzzle object containing all puzzle data\n * @throws {XdParseError} When the content is not valid XD format\n *\n * @example\n * ```typescript\n * import { parseXd } from 'xword-parser';\n * const xdData = 'Title: Example\\\\nAuthor: John Doe\\\\n\\\\n## Grid\\\\n...';\n * const puzzle = parseXd(xdData);\n * ```\n */\nexport function parseXd(content: string, options?: ParseOptions): XdPuzzle {\n const sections = splitIntoSections(content);\n\n if (sections.grid.length === 0) {\n throw new XdParseError('Invalid XD file: no grid section found', ErrorCode.XD_FORMAT_ERROR);\n }\n\n const metadata = parseMetadata(sections.metadata);\n const grid = parseGrid(sections.grid);\n const { across, down } = parseClues(sections.clues);\n\n // Validate grid is not empty\n if (grid.length === 0) {\n throw new XdParseError('Grid is empty', ErrorCode.XD_INVALID_GRID);\n }\n\n // Validate grid has consistent row lengths\n const width = grid[0]?.length || 0;\n if (width === 0) {\n throw new XdParseError('Grid has no columns', ErrorCode.XD_INVALID_GRID);\n }\n\n const height = grid.length;\n\n // Validate grid dimensions are within limits\n const maxWidth = options?.maxGridSize?.width ?? MAX_GRID_WIDTH;\n const maxHeight = options?.maxGridSize?.height ?? MAX_GRID_HEIGHT;\n\n if (width > maxWidth || height > maxHeight) {\n throw new XdParseError(\n `Grid dimensions too large: ${width}x${height}. Maximum supported size is ${maxWidth}x${maxHeight}`,\n ErrorCode.XD_INVALID_GRID,\n );\n }\n\n for (let i = 0; i < grid.length; i++) {\n if (!grid[i] || grid[i]?.length !== width) {\n throw new XdParseError(\n `Grid row ${i} has inconsistent width (expected ${width}, got ${grid[i]?.length || 0})`,\n ErrorCode.XD_INVALID_GRID,\n );\n }\n }\n\n const puzzle: XdPuzzle = {\n metadata,\n grid,\n across,\n down,\n };\n\n if (sections.notes.length > 0) {\n puzzle.notes = sections.notes.join('\\n').trim();\n }\n\n return puzzle;\n}\n\n/**\n * Convert an XD puzzle to the unified Puzzle format.\n *\n * @param puzzle - The XdPuzzle object to convert\n * @returns A unified Puzzle object\n *\n * @example\n * ```typescript\n * import { parseXd, convertXdToUnified } from 'xword-parser';\n * const xdPuzzle = parseXd(xdData);\n * const unifiedPuzzle = convertXdToUnified(xdPuzzle);\n * ```\n */\nexport function convertXdToUnified(puzzle: XdPuzzle): Puzzle {\n // Parser guarantees grid is not empty and has consistent width\n const firstRow = puzzle.grid[0];\n if (!firstRow) {\n // This should never happen as parser validates grid is not empty\n throw new Error('Invalid state: grid is empty');\n }\n\n const grid: Grid = {\n width: firstRow.length,\n height: puzzle.grid.length,\n cells: [],\n };\n\n // Convert grid - XD grid is string[][], we need to determine cell properties\n let cellNumber = 1;\n for (let y = 0; y < puzzle.grid.length; y++) {\n const row = puzzle.grid[y]!; // Parser guarantees all rows exist\n\n const cellRow: UnifiedCell[] = [];\n for (let x = 0; x < row.length; x++) {\n const cellValue = row[x]!; // Parser guarantees consistent width\n const isBlack = cellValue === '#';\n\n // Determine if this cell should have a number\n let number: number | undefined;\n if (!isBlack) {\n const needsNumber =\n // Start of across word\n ((x === 0 || puzzle.grid[y]![x - 1] === '#') &&\n x < row.length - 1 &&\n puzzle.grid[y]![x + 1] !== '#') ||\n // Start of down word\n ((y === 0 || puzzle.grid[y - 1]?.[x] === '#') &&\n y < puzzle.grid.length - 1 &&\n puzzle.grid[y + 1]?.[x] !== '#');\n\n if (needsNumber) {\n number = cellNumber++;\n }\n }\n\n cellRow.push({\n solution: isBlack ? undefined : cellValue,\n number,\n isBlack,\n });\n }\n grid.cells.push(cellRow);\n }\n\n // Convert clues\n const clues: Clues = {\n across: puzzle.across.map((c) => ({\n number: parseInt(c.number),\n text: c.clue,\n })),\n down: puzzle.down.map((c) => ({\n number: parseInt(c.number),\n text: c.clue,\n })),\n };\n\n const result: Puzzle = {\n title: puzzle.metadata.title,\n author: puzzle.metadata.author,\n copyright: puzzle.metadata.copyright,\n notes: puzzle.notes,\n date: puzzle.metadata.date,\n grid,\n clues,\n };\n\n // Add puzzle-level metadata if present\n const additionalProps: Record<string, unknown> = {};\n\n if (puzzle.metadata.editor) additionalProps.editor = puzzle.metadata.editor;\n\n // XD format also supports rebus metadata (different format than PUZ)\n if (puzzle.metadata.rebus) additionalProps.rebus = puzzle.metadata.rebus;\n if (puzzle.metadata.notepad) additionalProps.notepad = puzzle.metadata.notepad;\n\n if (Object.keys(additionalProps).length > 0) {\n result.additionalProperties = additionalProps;\n }\n\n return result;\n}\n"]}