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;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc