crossword-8.x-1.x-dev/src/Plugin/crossword/crossword_file_parser/CrosswordCompilerXmlParser.php
src/Plugin/crossword/crossword_file_parser/CrosswordCompilerXmlParser.php
<?php
namespace Drupal\crossword\Plugin\crossword\crossword_file_parser;
use Drupal\crossword\CrosswordException;
use Drupal\crossword\CrosswordFileParserPluginBase;
use Drupal\file\FileInterface;
use \SimpleXMLElement;
/**
* Plugin for parsing puzzle in Crossword Compiler xml.
*
* @CrosswordFileParser(
* id = "crossword_compiler_xml",
* title = @Translation("Crossword Compiler XML")
* )
*/
class CrosswordCompilerXmlParser extends CrosswordFileParserPluginBase {
/**
* {@inheritdoc}
*
* Checks for a xml file featuring an crossword-compiler tag.
*/
public static function isApplicable(FileInterface $file) {
if ($file->getMimeType() !== "application/xml") {
return FALSE;
}
if (strpos($file->getFilename(), ".xml") === FALSE) {
return FALSE;
}
$contents = file_get_contents($file->getFileUri());
if (strpos($contents, '<crossword-compiler') === FALSE) {
return FALSE;
};
return TRUE;
}
/**
* {@inheritdoc}
*/
public function parse() {
$xml = simplexml_load_string($this->contents);
if (empty($xml)) {
throw new CrosswordException('The file is corrupted.');
}
if (empty($xml->{'rectangular-puzzle'})) {
throw new CrosswordException('The grid is not rectangular.');
}
if (empty($xml->{'rectangular-puzzle'}->crossword)) {
throw new CrosswordException('The file is corrupted.');
}
if (empty($xml->{'rectangular-puzzle'}->crossword->clues)) {
throw new CrosswordException('There are no clues.');
}
$data = [
'id' => $this->file->id(),
'title' => $this->getTitle($xml),
'author' => $this->getAuthor($xml),
'notepad' => $this->getNotepad($xml),
'puzzle' => $this->getGridAndClues($xml),
];
return $data;
}
/**
* Returns title of crossword.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return string|null
* The title of the puzzle
*/
protected function getTitle(SimpleXMLElement $xml) {
if ($xml->{'rectangular-puzzle'}->metadata->title) {
return trim($xml->{'rectangular-puzzle'}->metadata->title->__toString());
}
}
/**
* Returns author of crossword.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return string|null
* The author of the puzzle
*/
protected function getAuthor(SimpleXMLElement $xml) {
if ($xml->{'rectangular-puzzle'}->metadata->creator) {
return trim($xml->{'rectangular-puzzle'}->metadata->creator->__toString());
}
}
/**
* Returns notepad from crossword, which is the description here.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return string|null
* The notepad of the puzzle
*/
protected function getNotepad(SimpleXMLElement $xml) {
if ($xml->{'rectangular-puzzle'}->metadata->description) {
return trim($xml->{'rectangular-puzzle'}->metadata->description->__toString());
}
}
/**
* Returns grid and clues.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return array
* Associative array containing nearly fully parsed grid and clues.
*/
protected function getGridAndClues(SimpleXMLElement $xml) {
$grid = [];
$clues = [
'across' => [],
'down' => [],
];
$raw_clues = $this->getRawClues($xml);
$raw_grid = $this->getRawGrid($xml);
$iterator = [
'index_across' => -1,
'index_down' => -1,
'numeral' => 0,
];
foreach ($raw_grid as $row_index => $raw_row) {
$row = [];
for ($col_index = 0; $col_index < count($raw_row); $col_index++) {
$fill = $raw_row[$col_index]['fill'];
$square = [
'row' => $row_index,
'col' => $col_index,
'circle' => $raw_row[$col_index]['circle'],
'rebus' => $fill && strlen($fill) > 1,
'fill' => $fill === NULL ? NULL : strtoupper($fill),
];
if ($raw_row[$col_index]['hint']) {
$square['hint'] = TRUE;
}
if (isset($raw_row[$col_index]['image'])) {
// Validate the image before adding to square.
$data = base64_decode($raw_row[$col_index]['image']['data']);
$embedded = @imagecreatefromstring($data);
if ($embedded !== FALSE) {
$square['image'] = $raw_row[$col_index]['image'];
imagedestroy($embedded);
}
else {
throw new CrosswordException('Puzzle contains a corrupted image.');
}
}
if ($fill !== NULL) {
// Init some things to NULL.
$numeral_incremented = FALSE;
$numeral = NULL;
// This will be the first square in an across clue if it is...
// 1. the left square or to the right of a black
// AND
// 2. not the right square and the square to its right is not black.
if ($col_index == 0 || $raw_row[$col_index - 1]['fill'] === NULL) {
if (isset($raw_row[$col_index + 1]) && $raw_row[$col_index + 1]['fill'] !== NULL) {
$iterator['index_across']++;
$iterator['numeral']++;
$numeral = $iterator['numeral'];
if (!isset($raw_clues['across'][$iterator['index_across']])) {
throw new CrosswordException('Number of across clues does not match size of grid.');
}
$clues['across'][] = [
'text' => $raw_clues['across'][$iterator['index_across']],
'numeral' => $iterator['numeral'],
];
$numeral_incremented = TRUE;
$square['across'] = [
'index' => $iterator['index_across'],
];
$square['numeral'] = $numeral;
}
else {
// In here? It's an uncrossed square. No across clue. No numeral.
}
}
else {
// In here? No numeral.
$square['across'] = [
'index' => $iterator['index_across'],
];
}
// This will be the first square in a down clue if...
// 1. It's the top square or the below a black
// AND
// 2. It's not the bottom square and the square below it is not black.
if ($row_index == 0 || $raw_grid[$row_index - 1][$col_index]['fill'] === NULL) {
if (isset($raw_grid[$row_index + 1][$col_index]) && $raw_grid[$row_index + 1][$col_index]['fill'] !== NULL) {
$iterator['index_down']++;
if (!$numeral_incremented) {
$iterator['numeral']++;
}
$numeral = $iterator['numeral'];
if (!isset($raw_clues['down'][$iterator['index_down']])) {
throw new CrosswordException('Number of down clues does not match size of grid.');
}
$clues['down'][] = [
'text' => $raw_clues['down'][$iterator['index_down']],
'numeral' => $iterator['numeral'],
];
$numeral_incremented = TRUE;
$square['down'] = [
'index' => $iterator['index_down'],
];
$square['numeral'] = $numeral;
}
else {
// In here? It's an uncrossed square. No down clue. No numeral.
}
}
else {
// In here? No numeral. Take the down value from the square above.
$square['down'] = $grid[$row_index - 1][$col_index]['down'];
}
}
$row[] = $square;
}
$grid[] = $row;
}
// Are there extra clues? The iterators should be at the end of the line.
if (count($raw_clues['down']) - 1 > $iterator['index_down']) {
throw new CrosswordException('Number of down clues does not match size of grid.');
}
if (count($raw_clues['across']) - 1 > $iterator['index_across']) {
throw new CrosswordException('Number of across clues does not match size of grid.');
}
return [
'grid' => $grid,
'clues' => $clues,
];
}
/**
* Returns an array of arrays of clue text.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return array
* Associative array containing an array of across clue text and an array
* of down clue text.
*/
protected function getRawClues(SimpleXMLElement $xml) {
$across = [];
foreach ($xml->{'rectangular-puzzle'}->crossword->clues[0]->clue as $clue) {
$across[] = $clue->__toString();
}
$down = [];
foreach ($xml->{'rectangular-puzzle'}->crossword->clues[1]->clue as $clue) {
$down[] = $clue->__toString();
}
if (empty($across)) {
throw new CrosswordException('The across clues are missing.');
}
if (empty($down)) {
throw new CrosswordException('The down clues are missing.');
}
return [
'across' => $across,
'down' => $down,
];
}
/**
* Returns a 2D array where each element is a square.
*
* @param \SimpleXMLElement $xml
* The xml.
*
* @return array
* 2D array where each element is the text of a square.
*/
protected function getRawGrid(SimpleXMLElement $xml) {
if (empty($xml->{'rectangular-puzzle'})) {
throw new CrosswordException('The grid is not rectangular.');
}
$raw_grid = [];
// Iterate through cells.
foreach ($xml->{'rectangular-puzzle'}->crossword->grid->cell as $cell) {
if (empty($cell->attributes()->x) || empty($cell->attributes()->y)) {
throw new CrosswordException('At least one cell is missing coordinates.');
}
$col = intval($cell->attributes()->x->__toString()) - 1;
$row = intval($cell->attributes()->y->__toString()) - 1;
if ($cell->attributes()->type && $cell->attributes()->type->__toString() == 'block') {
// A block can span multiple rows and columns. For example:
// <cell x="7-11" y="7-11" type="block">.
$col = $cell->attributes()->x->__toString();
$row = $cell->attributes()->y->__toString();
$col_span = explode("-", $col);
$row_span = explode("-", $row);
if (count($row_span) === 1) {
$row_span[] = $row_span[0];
}
$row_span[0] = intval($row_span[0]) - 1;
$row_span[1] = intval($row_span[1]) - 1;
if (count($col_span) === 1) {
$col_span[] = $col_span[0];
}
$col_span[0] = intval($col_span[0]) - 1;
$col_span[1] = intval($col_span[1]) - 1;
for ($row_index = $row_span[0]; $row_index <= $row_span[1]; $row_index++) {
if (!isset($raw_grid[$row_index])) {
$raw_grid[$row_index] = [];
}
for ($col_index = $col_span[0]; $col_index <= $col_span[1]; $col_index++) {
$raw_grid[$row_index][$col_index] = [
'fill' => NULL,
'circle' => NULL,
'hint' => NULL,
];
}
}
// Is there an image in this block?
if ($cell->{'background-picture'} && $cell->{'background-picture'}->{'encoded-image'}) {
$raw_grid[$row_span[0]][$col_span[0]]['image'] = [
'data' => $cell->{'background-picture'}->{'encoded-image'}->__toString(),
'format' => $cell->{'background-picture'}->attributes()->format->__toString(),
'width' => isset($col_span[1]) ? $col_span[1] - $col_span[0] + 1 : 1,
'height' => isset($row_span[1]) ? $row_span[1] - $row_span[0] + 1 : 1,
];
}
}
else {
// We subtract 1 because the xml indexes from 1 instead of zero.
$col = intval($cell->attributes()->x->__toString()) - 1;
$row = intval($cell->attributes()->y->__toString()) - 1;
if (!isset($raw_grid[$row])) {
$raw_grid[$row] = [];
}
if ($cell->attributes()->solution !== NULL) {
$fill = $cell->attributes()->solution->__toString();
$circle = !empty($cell->attributes()->{'background-shape'});
$hint = !empty($cell->attributes()->hint);
$raw_grid[$row][$col] = [
'fill' => $fill,
'circle' => $circle,
'hint' => $hint,
];
}
else {
throw new CrosswordException('At least one cell is missing its solution.');
}
}
}
// Confirm the grid is rectangular.
$grid_width = count($raw_grid[0]);
foreach ($raw_grid as $row_index => $row) {
if (count($row) !== $grid_width) {
// One of the rows differs in length.
throw new CrosswordException('The grid is not rectangular.');
}
$col_index = 0;
while ($col_index < count($row)) {
if (!array_key_exists($col_index, $row)) {
// Something is wrong with cell indexing.
throw new CrosswordException('The grid is not rectangular.');
}
$col_index++;
}
}
// Sort all the squares.
foreach ($raw_grid as &$raw_row) {
ksort($raw_row);
}
ksort($raw_grid);
return $raw_grid;
}
}
