@xwordly/xword-parser
Version:
Fast, type-safe TypeScript library for parsing crossword puzzles (PUZ, iPUZ, JPZ, XD)
323 lines (320 loc) • 11.6 kB
JavaScript
'use strict';
var chunk3QROV6K6_js = require('./chunk-3QROV6K6.js');
var chunkKVCCVFYY_js = require('./chunk-KVCCVFYY.js');
var fastXmlParser = require('fast-xml-parser');
function isJpzXmlNode(value) {
return typeof value === "object" && value !== null;
}
function isString(value) {
return typeof value === "string";
}
function parseStringAttribute(value) {
return isString(value) ? value : void 0;
}
function parseIntAttribute(value) {
if (isString(value)) {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? void 0 : parsed;
}
return void 0;
}
function parseMetadata(metadataNode) {
if (!isJpzXmlNode(metadataNode)) return {};
return {
title: parseStringAttribute(metadataNode.title),
creator: parseStringAttribute(metadataNode.creator) || parseStringAttribute(metadataNode.author),
copyright: parseStringAttribute(metadataNode.copyright),
description: parseStringAttribute(metadataNode.description),
publisher: parseStringAttribute(metadataNode.publisher),
identifier: parseStringAttribute(metadataNode.identifier)
};
}
function parseCells(gridNode, options) {
const cells = /* @__PURE__ */ new Map();
if (!isJpzXmlNode(gridNode)) {
throw new chunkKVCCVFYY_js.JpzParseError("Missing or invalid grid element", "JPZ_MISSING_GRID" /* JPZ_MISSING_GRID */);
}
const width = parseIntAttribute(gridNode["@_width"]);
const height = parseIntAttribute(gridNode["@_height"]);
if (width === void 0 || height === void 0) {
throw new chunkKVCCVFYY_js.JpzParseError(
"Grid dimensions (width and height) are required",
"JPZ_INVALID_GRID" /* JPZ_INVALID_GRID */
);
}
const maxWidth = options?.maxGridSize?.width ?? chunk3QROV6K6_js.MAX_GRID_WIDTH;
const maxHeight = options?.maxGridSize?.height ?? chunk3QROV6K6_js.MAX_GRID_HEIGHT;
if (width <= 0 || width > maxWidth || height <= 0 || height > maxHeight) {
throw new chunkKVCCVFYY_js.JpzParseError(
`Invalid grid dimensions: ${width}x${height}. Maximum supported size is ${maxWidth}x${maxHeight}`,
"JPZ_INVALID_GRID" /* JPZ_INVALID_GRID */,
{ details: { width, height } }
);
}
const cellNodes = gridNode.cell;
if (cellNodes) {
const cellArray = Array.isArray(cellNodes) ? cellNodes : [cellNodes];
for (const cellNode of cellArray) {
if (!isJpzXmlNode(cellNode)) continue;
const x = parseIntAttribute(cellNode["@_x"]);
const y = parseIntAttribute(cellNode["@_y"]);
if (x === void 0 || y === void 0) continue;
const key = `${x},${y}`;
const jpzCell = {
x,
y,
type: parseStringAttribute(cellNode["@_type"]) === "block" ? "block" : "cell",
solution: parseStringAttribute(cellNode["@_solution"]) || parseStringAttribute(cellNode["@_letter"]),
number: parseIntAttribute(cellNode["@_number"]),
isCircled: parseStringAttribute(cellNode["@_background-shape"]) === "circle",
backgroundColor: parseStringAttribute(cellNode["@_background-color"]),
barTop: parseStringAttribute(cellNode["@_top-bar"]) === "true",
barBottom: parseStringAttribute(cellNode["@_bottom-bar"]) === "true",
barLeft: parseStringAttribute(cellNode["@_left-bar"]) === "true",
barRight: parseStringAttribute(cellNode["@_right-bar"]) === "true"
};
cells.set(key, jpzCell);
}
}
return { cells, width, height };
}
function buildGrid(cells, width, height) {
const grid = [];
for (let y = 1; y <= height; y++) {
const row = [];
for (let x = 1; x <= width; x++) {
const key = `${x},${y}`;
const cell = cells.get(key);
if (cell) {
row.push(cell);
} else {
row.push({
x,
y,
type: "cell"
});
}
}
grid.push(row);
}
return grid;
}
function parseClues(cluesNode) {
const across = [];
const down = [];
if (!isJpzXmlNode(cluesNode)) {
return { across, down };
}
const clueArrays = Array.isArray(cluesNode) ? cluesNode : [cluesNode];
for (const clueSet of clueArrays) {
if (!isJpzXmlNode(clueSet)) continue;
let titleStr = "";
if (isString(clueSet.title)) {
titleStr = clueSet.title;
} else if (isJpzXmlNode(clueSet.title)) {
const titleNode = clueSet.title;
titleStr = parseStringAttribute(titleNode.b) || parseStringAttribute(titleNode["#text"]) || JSON.stringify(titleNode);
} else if (parseStringAttribute(clueSet["@_title"])) {
titleStr = parseStringAttribute(clueSet["@_title"]) || "";
}
const isAcross = titleStr.toLowerCase().includes("across");
const targetArray = isAcross ? across : down;
const clueNodes = clueSet.clue;
if (clueNodes) {
const clues = Array.isArray(clueNodes) ? clueNodes : [clueNodes];
for (const clue of clues) {
if (isString(clue)) {
const match = clue.match(/^(\d+)\.\s*(.+)$/);
if (match) {
targetArray.push({
number: match[1] || "",
text: match[2] || ""
});
}
} else if (isJpzXmlNode(clue)) {
const clueObj = {
number: parseStringAttribute(clue["@_number"]) || parseStringAttribute(clue.number) || "",
text: parseStringAttribute(clue["#text"]) || parseStringAttribute(clue.text) || parseStringAttribute(clue["@_text"]) || "",
format: parseStringAttribute(clue["@_format"])
};
const wordRef = parseStringAttribute(clue["@_word"]);
if (wordRef) {
clueObj.number = wordRef;
}
targetArray.push(clueObj);
}
}
}
}
return { across, down };
}
function parseWords(wordsNode) {
const words = [];
if (!isJpzXmlNode(wordsNode) || !wordsNode.word) {
return words;
}
const wordNodes = Array.isArray(wordsNode.word) ? wordsNode.word : [wordsNode.word];
for (const word of wordNodes) {
if (!isJpzXmlNode(word)) continue;
const jpzWord = {
id: parseStringAttribute(word["@_id"]) || "",
cells: []
};
if (isJpzXmlNode(word.cells)) {
const cellList = word.cells.cell;
if (cellList) {
const cells = Array.isArray(cellList) ? cellList : [cellList];
for (const cell of cells) {
if (!isJpzXmlNode(cell)) continue;
const x = parseIntAttribute(cell["@_x"]);
const y = parseIntAttribute(cell["@_y"]);
if (x !== void 0 && y !== void 0) {
jpzWord.cells.push({ x, y });
}
}
}
}
words.push(jpzWord);
}
return words;
}
function parseJpz(content, options) {
const parser = new fastXmlParser.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
textNodeName: "#text",
parseAttributeValue: false
});
let doc;
try {
doc = parser.parse(content);
} catch (e) {
throw new chunkKVCCVFYY_js.JpzParseError(
`Invalid XML: ${e instanceof Error ? e.message : "Unknown error"}`,
"JPZ_INVALID_XML" /* JPZ_INVALID_XML */,
void 0,
e
);
}
const puzzleRoot = doc["crossword-compiler-applet"] || doc["crossword-compiler"] || doc.puzzle || doc.crossword;
if (!isJpzXmlNode(puzzleRoot)) {
throw new chunkKVCCVFYY_js.JpzParseError("no recognized root element", "JPZ_INVALID_XML" /* JPZ_INVALID_XML */);
}
let rectangularPuzzle = puzzleRoot["rectangular-puzzle"];
if (!isJpzXmlNode(rectangularPuzzle) && isJpzXmlNode(puzzleRoot.puzzle)) {
const puzzleChild = puzzleRoot.puzzle;
if (isJpzXmlNode(puzzleChild)) {
rectangularPuzzle = puzzleChild["rectangular-puzzle"];
}
}
if (!isJpzXmlNode(rectangularPuzzle)) {
rectangularPuzzle = puzzleRoot;
}
if (rectangularPuzzle.coded) {
throw new chunkKVCCVFYY_js.UnsupportedPuzzleTypeError("Coded/cipher crosswords (Kaidoku)");
}
if (rectangularPuzzle.sudoku || rectangularPuzzle.kakuro) {
throw new chunkKVCCVFYY_js.UnsupportedPuzzleTypeError("Number puzzles");
}
if (rectangularPuzzle["word-search"] || rectangularPuzzle.wordsearch) {
throw new chunkKVCCVFYY_js.UnsupportedPuzzleTypeError("Word search");
}
const metadata = parseMetadata(rectangularPuzzle.metadata);
const crosswordNode = rectangularPuzzle.crossword || rectangularPuzzle.puzzle || rectangularPuzzle;
let gridNode = void 0;
if (isJpzXmlNode(crosswordNode)) {
gridNode = crosswordNode.grid || crosswordNode.Grid;
}
if (!gridNode && isJpzXmlNode(rectangularPuzzle)) {
gridNode = rectangularPuzzle.grid;
}
if (!gridNode) {
throw new chunkKVCCVFYY_js.JpzParseError("no grid found", "JPZ_INVALID_XML" /* JPZ_INVALID_XML */);
}
const { cells, width, height } = parseCells(gridNode, options);
const grid = buildGrid(cells, width, height);
let cluesNode = void 0;
if (isJpzXmlNode(crosswordNode)) {
cluesNode = crosswordNode.clues || crosswordNode.Clues;
}
if (!cluesNode && isJpzXmlNode(rectangularPuzzle)) {
cluesNode = rectangularPuzzle.clues || rectangularPuzzle.Clues;
}
const { across, down } = parseClues(cluesNode);
let wordsNode = void 0;
if (isJpzXmlNode(crosswordNode)) {
wordsNode = crosswordNode.words || crosswordNode.Words;
}
const words = wordsNode ? parseWords(wordsNode) : void 0;
return {
width,
height,
metadata,
grid,
across,
down,
words
};
}
function convertJpzToUnified(puzzle) {
const grid = {
width: puzzle.width,
height: puzzle.height,
cells: []
};
for (const row of puzzle.grid) {
const cellRow = [];
for (const cell of row) {
const unifiedCell = {
solution: cell.solution,
number: cell.number,
isBlack: cell.type === "block",
isCircled: cell.isCircled
};
if (cell.backgroundColor || cell.barTop || cell.barBottom || cell.barLeft || cell.barRight) {
unifiedCell.additionalProperties = {};
if (cell.backgroundColor) {
unifiedCell.additionalProperties.backgroundColor = cell.backgroundColor;
}
if (cell.barTop) unifiedCell.additionalProperties.barTop = cell.barTop;
if (cell.barBottom) unifiedCell.additionalProperties.barBottom = cell.barBottom;
if (cell.barLeft) unifiedCell.additionalProperties.barLeft = cell.barLeft;
if (cell.barRight) unifiedCell.additionalProperties.barRight = cell.barRight;
}
cellRow.push(unifiedCell);
}
grid.cells.push(cellRow);
}
const clues = {
across: puzzle.across.map((c) => ({
number: typeof c.number === "string" ? parseInt(c.number) : c.number,
text: c.text
})),
down: puzzle.down.map((c) => ({
number: typeof c.number === "string" ? parseInt(c.number) : c.number,
text: c.text
}))
};
const result = {
title: puzzle.metadata.title,
author: puzzle.metadata.creator,
copyright: puzzle.metadata.copyright,
grid,
clues
};
const additionalProps = {};
if (puzzle.metadata.description) additionalProps.description = puzzle.metadata.description;
if (puzzle.metadata.publisher) additionalProps.publisher = puzzle.metadata.publisher;
if (puzzle.metadata.identifier) additionalProps.identifier = puzzle.metadata.identifier;
if (puzzle.words && puzzle.words.length > 0) {
additionalProps.words = puzzle.words;
}
if (Object.keys(additionalProps).length > 0) {
result.additionalProperties = additionalProps;
}
return result;
}
exports.convertJpzToUnified = convertJpzToUnified;
exports.parseJpz = parseJpz;
//# sourceMappingURL=chunk-PFLLYQT4.js.map
//# sourceMappingURL=chunk-PFLLYQT4.js.map