Package Exports
- @xwordly/xword-parser
- @xwordly/xword-parser/lazy
- @xwordly/xword-parser/package.json
Readme
xword-parser
A TypeScript library for parsing popular crossword puzzle file formats into a unified, easy-to-use data structure.
Features
- Format Support: Parse PUZ, iPUZ, JPZ, and XD crossword formats
- Unified Data Model: All formats are converted to a common representation
- Type-Safe: Full TypeScript support with comprehensive type definitions
- Lightweight: Minimal runtime dependencies (only fast-xml-parser for JPZ support)
- Error Handling: Robust error handling with format-specific error classes
- Lazy Loading: Optional lazy-loading support to reduce bundle size
- Format Detection: Automatic format detection with optional filename hints
- Encoding Support: Configurable character encoding for text-based formats
Installation
npm install @xwordly/xword-parser
or
yarn add @xwordly/xword-parser
or
pnpm add @xwordly/xword-parser
Usage
Basic Example
import { parse } from '@xwordly/xword-parser';
import { readFileSync } from 'fs';
// Parse from file contents (auto-detects format)
const fileContent = readFileSync('puzzle.puz');
const puzzle = parse(fileContent);
console.log(puzzle.title);
console.log(puzzle.author);
console.log(puzzle.grid.width, 'x', puzzle.grid.height);
With Format Hints
Providing a filename helps with faster and more accurate format detection:
import { parse } from '@xwordly/xword-parser';
// Provide filename hint for better format detection
const puzzle = parse(fileContent, {
filename: 'crossword.puz'
});
// Specify encoding for text-based formats
const puzzle = parse(fileContent, {
filename: 'puzzle.ipuz',
encoding: 'latin1' // default is 'utf-8'
});
// Limit maximum grid size
const puzzle = parse(fileContent, {
maxGridSize: { width: 50, height: 50 }
});
Lazy Loading
For smaller bundle sizes in web applications, use the lazy-loading version:
import { parseLazy } from '@xwordly/xword-parser/lazy';
// Parsers are loaded dynamically only when needed
const puzzle = await parseLazy(fileContent);
Format-Specific Parsers
If you know the format in advance, you can use format-specific parsers:
import {
parseIpuz,
parsePuz,
parseJpz,
parseXd
} from '@xwordly/xword-parser';
// Use specific parser for known format
const ipuzPuzzle = parseIpuz(jsonString);
const puzPuzzle = parsePuz(binaryBuffer);
const jpzPuzzle = parseJpz(xmlString);
const xdPuzzle = parseXd(textString);
Parsing Different Formats
The library automatically detects the format based on the file contents:
import { parse } from '@xwordly/xword-parser';
// Parse PUZ format (binary)
const puzData = await fetch('https://example.com/puzzle.puz')
.then(res => res.arrayBuffer());
const puzPuzzle = parse(puzData);
// Parse iPUZ format (JSON)
const ipuzData = await fetch('https://example.com/puzzle.ipuz')
.then(res => res.text());
const ipuzPuzzle = parse(ipuzData);
// Parse JPZ format (XML)
const jpzData = await fetch('https://example.com/puzzle.jpz')
.then(res => res.text());
const jpzPuzzle = parse(jpzData);
// Parse XD format (text)
const xdData = await fetch('https://example.com/puzzle.xd')
.then(res => res.text());
const xdPuzzle = parse(xdData);
API Reference
Main Functions
parse(data: string | Buffer | ArrayBuffer, options?: ParseOptions): Puzzle
Parses crossword puzzle data from various formats. This is a pure, synchronous function.
Parameters:
data
: The puzzle data as a string (for text formats) or binary data (for PUZ format)options
(optional):filename
: Hint for format detection (e.g., "puzzle.puz")encoding
: Character encoding for text formats (default: "utf-8")maxGridSize
: Maximum allowed grid dimensions (e.g.,{width: 50, height: 50}
)
Returns: A Puzzle
object
Throws:
FormatDetectionError
if the format cannot be detectedParseError
for general parsing errorsIpuzParseError
,PuzParseError
,JpzParseError
, orXdParseError
for format-specific errorsUnsupportedPuzzleTypeError
if the puzzle type is not a crossword
parseLazy(data: string | Buffer | ArrayBuffer, options?: ParseOptions): Promise<Puzzle>
Lazy-loading version of parse()
that loads parsers dynamically.
Parameters: Same as parse()
Returns: A Promise that resolves to a Puzzle
object
Throws: Same errors as parse()
Format-Specific Functions
Each format has its own parse and convert functions:
parseIpuz(content: string | Buffer, options?: ParseOptions): IpuzPuzzle
parsePuz(data: Buffer | ArrayBuffer | Uint8Array | string, options?: ParseOptions): PuzPuzzle
parseJpz(content: string, options?: ParseOptions): JpzPuzzle
parseXd(content: string, options?: ParseOptions): XdPuzzle
And corresponding converters:
convertIpuzToUnified(puzzle: IpuzPuzzle): Puzzle
convertPuzToUnified(puzzle: PuzPuzzle): Puzzle
convertJpzToUnified(puzzle: JpzPuzzle): Puzzle
convertXdToUnified(puzzle: XdPuzzle): Puzzle
Supported Formats
PUZ Format
The .puz
format is a binary format created by Across Lite. It's one of the most common crossword formats and includes:
- Grid layout and solutions
- Across and Down clues
- Metadata (title, author, copyright)
- Optional features like rebuses and circles
iPUZ Format
The .ipuz
format is a JSON-based open standard that supports:
- Standard crosswords
- Variety puzzles (cryptics, acrostics, etc.)
- Rich metadata
- Styled cells and advanced features
- Unicode support
JPZ Format
The .jpz
format is an XML-based format used by Crossword Compiler. Features include:
- Complete puzzle data
- Timer and solving information
- Publishing metadata
- Support for various puzzle types
XD Format
The .xd
format is a simple text-based format that's human-readable and includes:
- Grid representation using text
- Simple clue format
- Basic metadata
- Easy to create and edit manually
Data Types
Puzzle Interface
interface Puzzle {
title?: string;
author?: string;
copyright?: string;
notes?: string;
date?: string;
grid: Grid;
clues: Clues;
rebusTable?: Map<number, string>;
additionalProperties?: Record<string, unknown>;
}
Grid and Cell Types
interface Grid {
width: number;
height: number;
cells: Cell[][];
}
interface Cell {
solution?: string;
number?: number;
isBlack: boolean;
isCircled?: boolean;
hasRebus?: boolean;
rebusKey?: number;
}
Clue Types
interface Clues {
across: Clue[];
down: Clue[];
}
interface Clue {
number: number;
text: string;
}
Error Handling
The library provides specific error classes for different scenarios:
ParseError
: Base class for all parsing errorsFormatDetectionError
: Unable to detect the puzzle formatIpuzParseError
: iPUZ-specific parsing errorsPuzParseError
: PUZ-specific parsing errorsJpzParseError
: JPZ-specific parsing errorsXdParseError
: XD-specific parsing errorsUnsupportedPuzzleTypeError
: When a file contains a non-crossword puzzleInvalidFileError
: General file format issues
All error classes extend ParseError
and include error codes for programmatic handling:
try {
const puzzle = parse(data);
} catch (error) {
if (error instanceof IpuzParseError) {
console.error('iPUZ parsing failed:', error.message);
console.error('Error code:', error.code);
}
}
Library Architecture
The library is designed with the following principles:
- Sans I/O: All parsers are pure functions with no side effects or file I/O
- Format-First Parsing: Each parser first captures all format-specific data, then converts to the unified format
- Type Safety: Comprehensive TypeScript types for both format-specific and unified structures
- Error Recovery: Smart error handling that distinguishes between format mismatches and real parsing errors
- Extensibility: Easy to add new formats by implementing the parser/converter pattern
Development
Building
npm run build # Build for production
npm run dev # Build with watch mode
Testing
npm test # Run tests once
npm run test:watch # Run tests in watch mode
npm run test:coverage # Generate coverage report
The test suite includes:
- Unit tests for each format parser
- Property-based testing with fast-check
- Performance benchmarks
- Integration tests with real puzzle files
Linting & Formatting
npm run lint # Check for linting errors
npm run lint:fix # Fix linting errors
npm run format # Format code with Prettier
npm run typecheck # Type-check without building
Requirements
- Node.js >= 18
- TypeScript >= 5.3 (for development)