@xwordly/xword-parser
Version:
Fast, type-safe TypeScript library for parsing crossword puzzles (PUZ, iPUZ, JPZ, XD)
231 lines (228 loc) • 6.86 kB
JavaScript
;
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