crossword-8.x-1.x-dev/src/Plugin/crossword/crossword_file_parser/AcrossLiteTextParser.php
src/Plugin/crossword/crossword_file_parser/AcrossLiteTextParser.php
<?php
namespace Drupal\crossword\Plugin\crossword\crossword_file_parser;
use Drupal\crossword\CrosswordException;
use Drupal\crossword\CrosswordFileParserPluginBase;
use Drupal\file\FileInterface;
/**
* Plugin for parsing files in the Across Lite Text format v1 or v2.
*
* @CrosswordFileParser(
* id = "across_lite_text",
* title = @Translation("Across Lite Text")
* )
*/
class AcrossLiteTextParser extends CrosswordFileParserPluginBase {
/**
* {@inheritdoc}
*
* Checks for a text file featuring an ACROSS PUZZLE tag.
*/
public static function isApplicable(FileInterface $file) {
if ($file->getMimeType() !== "text/plain") {
return FALSE;
}
if (strpos($file->getFilename(), ".txt") === FALSE) {
return FALSE;
}
$contents = file_get_contents($file->getFileUri());
$contents = trim($contents);
// This can also parse ACROSS PUZZLE 2.
if (strpos($contents, '<ACROSS PUZZLE') === FALSE) {
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function parse() {
$lines = explode("\n", $this->contents);
foreach ($lines as &$line) {
$line = trim($line);
}
$pre_parse = $this->parseTags($lines);
$data = [
'id' => $this->file->id(),
'title' => $this->getTitle($pre_parse),
'author' => $this->getAuthor($pre_parse),
'notepad' => $this->getNotepad($pre_parse),
'puzzle' => $this->getGridAndClues($pre_parse),
];
return $data;
}
/**
* Convert the txt file into a n associative array, more or less.
*
* @param array $lines
* The trimmed lines of the file.
*
* @return array
* An array containing a rudimentary parsing of the crossword.
*/
protected function parseTags(array $lines) {
$tags = [
'title' => '<TITLE>',
'author' => '<AUTHOR>',
'copyright' => '<COPYRIGHT>',
'size' => '<SIZE>',
'grid' => '<GRID>',
'rebus' => '<REBUS>',
'across' => '<ACROSS>',
'down' => '<DOWN>',
'notepad' => '<NOTEPAD>',
];
$pre_parse = [];
$current_tag = '';
foreach ($lines as $line) {
if (!empty($line) && in_array($line, $tags, TRUE)) {
$current_tag = array_search($line, $tags);
}
elseif ($current_tag) {
$pre_parse[$current_tag][] = $line;
}
}
if (!isset($pre_parse['grid']) || empty($pre_parse['grid'])) {
throw new CrosswordException('The grid is missing.');
}
if (!isset($pre_parse['across']) || empty($pre_parse['across'])) {
throw new CrosswordException('The across clues are missing.');
}
if (!isset($pre_parse['down']) || empty($pre_parse['down'])) {
throw new CrosswordException('The down clues are missing.');
}
return $pre_parse;
}
/**
* Returns title of crossword.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return string|null
* The title of the puzzle
*/
protected function getTitle(array $pre_parse) {
if (isset($pre_parse['title'])) {
return $pre_parse['title'][0];
}
}
/**
* Returns author of crossword.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return string|null
* The author of the puzzle
*/
protected function getAuthor(array $pre_parse) {
if (isset($pre_parse['author'])) {
return $pre_parse['author'][0];
}
}
/**
* Returns notepad from crossword.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return string|null
* The notepad of the puzzle
*/
protected function getNotepad(array $pre_parse) {
if (isset($pre_parse['notepad'])) {
return $pre_parse['notepad'][0];
}
}
/**
* Returns grid and clues.
*
* When returns, the squares don't have moves and the references
* don't have the index added yet.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return array
* Associative array containing nearly fully parsed grid and clues.
*/
protected function getGridAndClues(array $pre_parse) {
$grid = [];
$clues = [
'across' => [],
'down' => [],
];
$raw_clues = $this->getRawClues($pre_parse);
$raw_grid = $this->getRawGrid($pre_parse);
$iterator = [
'index_across' => -1,
'index_down' => -1,
'numeral' => 0,
];
$rebus_array = $this->getRebusArray($pre_parse);
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];
// In text, lowercase letters indicate a circle.
$circle = $fill && strtoupper($fill) != $fill;
$square = [
'row' => $row_index,
'col' => $col_index,
'circle' => $circle,
'rebus' => FALSE,
'fill' => $fill === NULL ? NULL : strtoupper($fill),
];
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] === NULL) {
if (isset($raw_row[$col_index + 1]) && $raw_row[$col_index + 1] !== 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] === NULL) {
if (isset($raw_grid[$row_index + 1][$col_index]) && $raw_grid[$row_index + 1][$col_index] !== 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'];
}
}
// Is it a rebus square?
if (is_numeric($square['fill']) && !empty($rebus_array) && isset($rebus_array[$square['fill']])) {
$square['fill'] = $rebus_array[$square['fill']];
$square['rebus'] = TRUE;
}
$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 array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return array
* Associative array containing an array of across clue text and an array
* of down clue text.
*/
protected function getRawClues(array $pre_parse) {
return [
'across' => $pre_parse['across'],
'down' => $pre_parse['down'],
];
}
/**
* Returns a 2D array where each element is the text of a square.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return array
* 2D array where each element is the text of a square.
*/
protected function getRawGrid(array $pre_parse) {
$raw_grid = [];
$grid_lines = $pre_parse['grid'];
$first_row_columns = strlen($grid_lines[0]);
foreach ($grid_lines as $row_index => $grid_line) {
if (strlen($grid_line) !== $first_row_columns) {
throw new CrosswordException('The grid is not rectangular.');
}
for ($col_index = 0; $col_index < strlen($grid_line); $col_index++) {
$raw_grid[$row_index][] = $grid_line[$col_index] !== "." ? $grid_line[$col_index] : NULL;
}
}
return $raw_grid;
}
/**
* Returns array used to handle rebus puzzles.
*
* @param array $pre_parse
* An array containing a rudimentary parsing of the crossword.
*
* @return array
* An associative array. The key is a number. The corresponding value
* is the text that should replace the number any time it appears in the
* grid.
*/
protected function getRebusArray(array $pre_parse) {
if (isset($pre_parse['rebus'])) {
$rebus_array = [];
foreach ($pre_parse['rebus'] as $line) {
$parts = explode(':', $line);
if (is_numeric($parts[0]) && isset($parts[1])) {
$rebus_array[$parts[0]] = $parts[1];
}
}
return $rebus_array;
}
}
}
