@xwordly/xword-parser
Version:
Fast, type-safe TypeScript library for parsing crossword puzzles (PUZ, iPUZ, JPZ, XD)
431 lines (429 loc) • 14.2 kB
JavaScript
import { MAX_GRID_WIDTH, MAX_GRID_HEIGHT } from './chunk-GKUILQIP.mjs';
import { IpuzParseError, UnsupportedPuzzleTypeError } from './chunk-O732NFC7.mjs';
// src/ipuz.ts
var CellType = /* @__PURE__ */ ((CellType2) => {
CellType2["NORMAL"] = "normal";
CellType2["BLOCK"] = "block";
CellType2["NULL"] = "null";
return CellType2;
})(CellType || {});
function parseCellFromIpuz(cellData, solutionData) {
const cell = {
type: "normal" /* NORMAL */
};
if (cellData === null || cellData === "null") {
cell.type = "null" /* NULL */;
return cell;
}
if (cellData === "#") {
cell.type = "block" /* BLOCK */;
return cell;
}
if (typeof cellData === "object" && cellData !== null) {
const cellObj = cellData;
if ("cell" in cellObj && cellObj.cell !== void 0) {
const baseValue = cellObj.cell;
if (baseValue === "#") {
cell.type = "block" /* BLOCK */;
} else if (baseValue === "null" || baseValue === null) {
cell.type = "null" /* NULL */;
} else if (typeof baseValue === "number" || typeof baseValue === "string") {
if (baseValue !== 0 && baseValue !== "0") {
cell.number = baseValue;
}
}
}
if ("style" in cellObj && cellObj.style && typeof cellObj.style === "object") {
cell.style = {};
const styleFields = [
"shapebg",
"highlight",
"named",
"border",
"divided",
"label",
"mark",
"imagebg",
"color",
"colortext",
"colorborder",
"colorbar",
"barred",
"dotted",
"dashed",
"lessthan",
"greaterthan",
"equal",
"handler",
"handlerdata"
];
for (const field of styleFields) {
if (field in cellObj.style) {
const value = cellObj.style[field];
if (value !== void 0) {
cell.style[field] = value;
}
}
}
}
if ("value" in cellObj && cellObj.value !== void 0) {
cell.value = cellObj.value;
}
if ("continued" in cellObj && cellObj.continued !== void 0) {
cell.continued = cellObj.continued;
}
if ("directions" in cellObj && cellObj.directions !== void 0) {
cell.directions = cellObj.directions;
}
if ("given" in cellObj && cellObj.given !== void 0) {
cell.given = cellObj.given;
}
} else if (typeof cellData === "number" || typeof cellData === "string") {
if (typeof cellData === "number" && cellData > 0) {
cell.number = cellData;
} else if (typeof cellData === "string") {
if (cellData === "0" || cellData === "#" || cellData === "null") {
if (cellData === "#") {
cell.type = "block" /* BLOCK */;
}
} else {
const numVal = Number.parseInt(cellData, 10);
if (!Number.isNaN(numVal) && numVal > 0) {
cell.number = cellData;
} else {
cell.value = cellData;
}
}
}
}
if (solutionData) {
if (typeof solutionData === "string" && solutionData !== "#" && solutionData !== "null" && solutionData !== null) {
cell.solution = solutionData;
}
}
return cell;
}
function parseCluesFromIpuz(cluesData) {
const clues = {};
for (const [direction, clueList] of Object.entries(cluesData)) {
if (!Array.isArray(clueList)) {
continue;
}
clues[direction] = [];
for (const clueItem of clueList) {
let clue = null;
if (Array.isArray(clueItem) && clueItem.length >= 2) {
clue = {
number: clueItem[0],
text: clueItem[1]
};
if (clueItem.length > 2) {
for (let i = 2; i < clueItem.length; i++) {
const extra = clueItem[i];
if (typeof extra === "object" && extra !== null) {
const extraObj = extra;
if ("references" in extraObj && extraObj.references) {
clue.references = extraObj.references;
}
if ("continued" in extraObj && extraObj.continued) {
clue.continued = extraObj.continued;
}
if ("highlight" in extraObj && extraObj.highlight) {
clue.highlight = extraObj.highlight;
}
if ("image" in extraObj && extraObj.image) {
clue.image = extraObj.image;
}
}
}
}
} else if (typeof clueItem === "object" && clueItem !== null) {
const clueObj = clueItem;
clue = {
number: clueObj.number || "",
text: clueObj.clue || ""
};
if (clueObj.cells) {
clue.cells = clueObj.cells;
}
if (clueObj.references) {
clue.references = clueObj.references;
}
if (clueObj.continued) {
clue.continued = clueObj.continued;
}
if (clueObj.highlight !== void 0) {
clue.highlight = clueObj.highlight;
}
if (clueObj.image) {
clue.image = clueObj.image;
}
}
if (clue) {
clues[direction].push(clue);
}
}
}
return clues;
}
function parseIpuz(content, options) {
const stringContent = typeof content === "string" ? content : content.toString("utf-8");
let jsonContent = stringContent.trim();
if (jsonContent.startsWith("ipuz(") && jsonContent.endsWith(")")) {
jsonContent = jsonContent.slice(5, -1);
}
let data;
try {
data = JSON.parse(jsonContent);
} catch (e) {
throw new IpuzParseError(
`Invalid JSON: ${e instanceof Error ? e.message : "Unknown error"}`,
"IPUZ_INVALID_JSON" /* IPUZ_INVALID_JSON */,
void 0,
e
);
}
if (!data.kind || !Array.isArray(data.kind) || !data.kind.some((k) => k.includes("crossword"))) {
throw new UnsupportedPuzzleTypeError("Non-crossword");
}
if (!data.dimensions || typeof data.dimensions !== "object") {
throw new IpuzParseError(
"Missing or invalid dimensions field",
"IPUZ_MISSING_REQUIRED_FIELD" /* IPUZ_MISSING_REQUIRED_FIELD */
);
}
if (!data.dimensions.width || !data.dimensions.height) {
throw new IpuzParseError(
"Missing width or height in dimensions",
"IPUZ_MISSING_REQUIRED_FIELD" /* IPUZ_MISSING_REQUIRED_FIELD */
);
}
if (typeof data.dimensions.width !== "number" || typeof data.dimensions.height !== "number") {
throw new IpuzParseError("Width and height must be numbers", "IPUZ_INVALID_DATA_TYPE" /* IPUZ_INVALID_DATA_TYPE */);
}
if (!Number.isInteger(data.dimensions.width) || !Number.isInteger(data.dimensions.height)) {
throw new IpuzParseError("Width and height must be integers", "IPUZ_INVALID_DATA_TYPE" /* IPUZ_INVALID_DATA_TYPE */);
}
if (data.dimensions.width <= 0 || data.dimensions.height <= 0) {
throw new IpuzParseError(
"Width and height must be positive numbers",
"IPUZ_INVALID_GRID_SIZE" /* IPUZ_INVALID_GRID_SIZE */
);
}
const maxWidth = options?.maxGridSize?.width ?? MAX_GRID_WIDTH;
const maxHeight = options?.maxGridSize?.height ?? MAX_GRID_HEIGHT;
if (data.dimensions.width > maxWidth || data.dimensions.height > maxHeight) {
throw new IpuzParseError(
`Grid dimensions too large: ${data.dimensions.width}x${data.dimensions.height}. Maximum supported size is ${maxWidth}x${maxHeight}`,
"IPUZ_INVALID_GRID_SIZE" /* IPUZ_INVALID_GRID_SIZE */
);
}
if (!data.puzzle || !Array.isArray(data.puzzle)) {
throw new IpuzParseError(
"Missing or invalid puzzle grid",
"IPUZ_MISSING_REQUIRED_FIELD" /* IPUZ_MISSING_REQUIRED_FIELD */
);
}
const puzzle = {
version: data.version || "",
kind: data.kind || [],
dimensions: data.dimensions,
puzzle: [],
clues: {},
extensions: {}
};
const simpleFields = [
"title",
"author",
"copyright",
"publisher",
"publication",
"url",
"intro",
"explanation",
"annotation",
"notes",
"difficulty",
"origin",
"date",
"empty",
"charset",
"block",
"answer"
];
for (const field of simpleFields) {
if (field in data && data[field] !== void 0) {
puzzle[field] = data[field];
}
}
if ("uniqueid" in data && data.uniqueid !== void 0) {
puzzle.uniqueId = data.uniqueid;
}
if ("showenumerations" in data && data.showenumerations !== void 0) {
puzzle.showEnumerations = data.showenumerations;
}
if ("clueplacement" in data && data.clueplacement !== void 0) {
puzzle.cluePlacement = data.clueplacement;
}
if ("answers" in data && data.answers) {
puzzle.answers = data.answers;
}
if ("enumeration" in data && data.enumeration !== void 0) {
puzzle.enumeration = data.enumeration;
}
if ("enumerations" in data && data.enumerations) {
puzzle.enumerations = data.enumerations;
}
if ("volatile" in data && data.volatile) {
puzzle.volatile = data.volatile;
}
if ("checksum" in data && data.checksum) {
puzzle.checksum = data.checksum;
}
if ("zones" in data && data.zones) {
puzzle.zones = data.zones;
}
if ("styles" in data && data.styles) {
puzzle.styles = data.styles;
}
if ("misses" in data && data.misses) {
puzzle.misses = data.misses;
}
const solutionGrid = data.solution || [];
if (solutionGrid.length > 0) {
puzzle.solution = solutionGrid;
}
if ("saved" in data && data.saved) {
puzzle.saved = data.saved;
}
const puzzleGrid = data.puzzle || [];
for (let rowIdx = 0; rowIdx < puzzleGrid.length; rowIdx++) {
const row = puzzleGrid[rowIdx];
if (!row) continue;
const cellRow = [];
for (let colIdx = 0; colIdx < row.length; colIdx++) {
const cellData = row[colIdx];
let solutionVal = void 0;
if (solutionGrid.length > rowIdx && solutionGrid[rowIdx] && solutionGrid[rowIdx].length > colIdx) {
solutionVal = solutionGrid[rowIdx][colIdx];
}
const cell = parseCellFromIpuz(cellData, solutionVal);
cellRow.push(cell);
}
puzzle.puzzle.push(cellRow);
}
if ("clues" in data && data.clues) {
puzzle.clues = parseCluesFromIpuz(data.clues);
}
for (const [key, value] of Object.entries(data)) {
if (key.startsWith("http://") || key.startsWith("https://") || key.includes(":")) {
puzzle.extensions[key] = value;
}
}
return puzzle;
}
function convertIpuzToUnified(puzzle) {
const grid = {
width: puzzle.dimensions.width,
height: puzzle.dimensions.height,
cells: []
};
for (let y = 0; y < puzzle.dimensions.height; y++) {
const row = [];
for (let x = 0; x < puzzle.dimensions.width; x++) {
const ipuzCell = puzzle.puzzle[y]?.[x];
const solutionCell = puzzle.solution?.[y]?.[x];
const cellNumber = ipuzCell?.number ? typeof ipuzCell.number === "string" ? parseInt(ipuzCell.number) : ipuzCell.number : void 0;
const isBlack = ipuzCell?.type === "block" /* BLOCK */;
const solution = typeof solutionCell === "string" ? solutionCell : ipuzCell?.value || void 0;
const cell = {
solution,
number: cellNumber,
isBlack
};
if (ipuzCell?.style && typeof ipuzCell.style === "object" && ipuzCell.style.shapebg === "circle") {
cell.isCircled = true;
}
if (ipuzCell?.style || ipuzCell?.continued || ipuzCell?.directions || ipuzCell?.given) {
cell.additionalProperties = {};
if (ipuzCell.style && typeof ipuzCell.style === "object" && (Object.keys(ipuzCell.style).length > 1 || ipuzCell.style.shapebg !== "circle")) {
cell.additionalProperties.style = ipuzCell.style;
}
if (ipuzCell.continued) {
cell.additionalProperties.continued = ipuzCell.continued;
}
if (ipuzCell.directions) {
cell.additionalProperties.directions = ipuzCell.directions;
}
if (ipuzCell.given !== void 0) {
cell.additionalProperties.given = ipuzCell.given;
}
if (Object.keys(cell.additionalProperties).length === 0) {
delete cell.additionalProperties;
}
}
row.push(cell);
}
grid.cells.push(row);
}
const clues = {
across: [],
down: []
};
if (puzzle.clues?.Across) {
for (const clue of puzzle.clues.Across) {
const clueNumber = typeof clue.number === "string" ? parseInt(clue.number) : clue.number;
if (!Number.isNaN(clueNumber) && clueNumber > 0) {
clues.across.push({
number: clueNumber,
text: clue.text || ""
});
}
}
}
if (puzzle.clues?.Down) {
for (const clue of puzzle.clues.Down) {
const clueNumber = typeof clue.number === "string" ? parseInt(clue.number) : clue.number;
if (!Number.isNaN(clueNumber) && clueNumber > 0) {
clues.down.push({
number: clueNumber,
text: clue.text || ""
});
}
}
}
const result = {
title: puzzle.title,
author: puzzle.author,
copyright: puzzle.copyright,
notes: puzzle.notes,
date: puzzle.date,
grid,
clues
};
const additionalProps = {};
if (puzzle.difficulty) additionalProps.difficulty = puzzle.difficulty;
if (puzzle.publisher) additionalProps.publisher = puzzle.publisher;
if (puzzle.publication) additionalProps.publication = puzzle.publication;
if (puzzle.url) additionalProps.url = puzzle.url;
if (puzzle.uniqueId) additionalProps.uniqueId = puzzle.uniqueId;
if (puzzle.intro) additionalProps.intro = puzzle.intro;
if (puzzle.explanation) additionalProps.explanation = puzzle.explanation;
if (puzzle.annotation) additionalProps.annotation = puzzle.annotation;
if (puzzle.styles && Object.keys(puzzle.styles).length > 0) {
additionalProps.styles = puzzle.styles;
}
if (puzzle.zones && puzzle.zones.length > 0) {
additionalProps.zones = puzzle.zones;
}
if (Object.keys(puzzle.extensions).length > 0) {
additionalProps.extensions = puzzle.extensions;
}
if (Object.keys(additionalProps).length > 0) {
result.additionalProperties = additionalProps;
}
return result;
}
export { CellType, convertIpuzToUnified, parseIpuz };
//# sourceMappingURL=chunk-VJXS4EY5.mjs.map
//# sourceMappingURL=chunk-VJXS4EY5.mjs.map