source = str_replace(array("\r\n","\r"), "\n", $source); $this->size = strlen($this->source); } function remaining () { return $this->size - $this->pos; } function consume ($count = 1) { $val = ''; for($i = 0; $i < $count; $i++) { if ($this->eof()) throw new Exception('End of file reached during read.', 801); $val = $val . $this->source[$this->pos]; $this->pos++; } return $val; } function consumeUntil ($char, $inclusive = false, $stopAtEOF = false) { $skipped = ''; while($this->source[$this->pos] !== $char) { if ($this->eof()) { if ($stopAtEOF) return $skipped; throw new Exception('End of file reached during read.', 801); } $skipped = $skipped . $this->source[$this->pos]; $this->pos++; } if ($inclusive) { $skipped = $skipped . $char; $this->pos++; } return $skipped; } function consumeWhile ($char) { $skipped = ''; while($this->source[$this->pos] === $char) { if ($this->eof()) throw new Exception('End of file reached during read.', 801); $skipped = $skipped . $this->source[$this->pos]; $this->pos++; } return $skipped; } function eof () { return $this->pos >= $this->size; } function getChar () { return $this->source[$this->pos]; } function lookAhead ($steps) { if (($this->pos + $steps) > $this->size) { throw new Exception('End of file reached during read.', 801); } $result = ""; $i = $this->pos; for (; $i < ($this->pos + $steps); $i++) { $result = $result . $this->source[$i]; } return $result; } function lookFoward ($pos) { if ($pos + $this->pos > $this->size) { throw new Exception('End of file reached during read.', 801); } return $this->source[$pos + $this->pos]; } function lookBack ($steps) { if (($this->pos - $steps) < 0) { throw new Exception('End of file reached during read.', 801); } $result = ""; $i = $this->pos; for (; $i > ($this->pos - $steps); $i--) { $result = $result . $this->source[$i]; } return $result; } } class Parser { private $reader; private $line = 1; function __construct ($reader) { $this->reader = $reader; } function skipBlanklines () { while (in_array($this->reader->getChar(), array(" ", "\n"))) { $this->reader->consumeWhile(' '); if ($this->reader->getChar() === "\n") { $this->line++; $this->reader->consume(); } } } function parse () { $numbers = array(); $questions = array(); try { while (!$this->reader->eof()) { $this->skipBlanklines(); if($this->reader->eof()) break; $char = $this->reader->getChar(); if ($char === '#') { // skip comment $this->reader->consumeUntil("\n", true); $this->line++; continue; } $char = $this->reader->consume(); if ($char === ':') { $foward = $this->reader->getChar(); if($foward !== ':') { $actual = $char.$foward; throw new Exception("Error na linha $this->line. ${actual} não é uma expressão válida, talvez você quisesse '::' (quebra de página )?", 800); } $this->reader->consumeUntil("\n", true); $this->line++; $pagebreak = new stdClass; $pagebreak->type = 'PAGEBREAK'; $questions[] = $pagebreak; continue; } if (is_numeric($char)) { $foward = $this->reader->getChar(); if (is_numeric($foward)) { // two digits $char = $char . $foward; $this->reader->consume(); $foward = $this->reader->getChar(); } if ($foward !== '.') { throw new Exception("Error na linha $this->line. O número da questão deve ser seguido de um .(ponto).", 800); } if (in_array($char, $numbers)) { throw new Exception('Error na linha '.($this->line).'. O número '.$char.' já foi utilizado anteriormente.', 802); } $numbers[] = $char; // read '.' $this->reader->consume(); $questions[] = $this->parseQuestion($char); } else { throw new Exception('Error na linha '.($this->line).'. Você deve informar uma questão nessa linha', 800); } } } catch (Exception $e) { $message = $e->getMessage(); // echo $e->getTraceAsString(); throw new Exception ("Erro de processamento: $message Total de linhas processadas: $this->line", 800); } if (count($questions) == 0) { throw new Exception('Error na linha '.($this->line).'. Questionário deve ter ao menos uma questão.', 806); } return $questions; } function parseQuestion ($number) { $question = new stdClass(); $question->number = 'q'.$number; // params $this->reader->consumeWhile(' '); if ($this->reader->getChar() !== '[') { throw new Exception('Error na linha '.($this->line).'. Toda questão deve ter seus parâmetros (M, D, S, I e *) definidos entre [ ].', 800); } $this->reader->consume(); $params = $this->reader->consumeUntil("]"); $params = str_replace(' ', '', $params); $validTypes = ['M','D','S', 'I']; if (strlen($params) === 0) { throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803); } $found = false; foreach ($validTypes as $type) { if (strpos($params, $type) !== FALSE) { if ($found) { throw new Exception('Error na linha '.($this->line).'. Questões devem ter apenas um parâmetro de tipo: M, D, I ou S.', 803); } $question->type = $type; $found = true; } } if (!$found) { throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803); } $params = str_replace($validTypes, '', $params); if(strlen($params) > 0) { if (strpos($params, '*') !== FALSE) { $question->required = true; $params = str_replace('*', '', $params); } else { throw new Exception('Error na linha '.($this->line).'. Parâmetro desconhecido:'.$params.'. Os parâmetros válidos são: M, D, S, I e *.', 800); } } if(strlen($params) > 0) { throw new Exception('Error na linha '.($this->line).'. Parâmetro desconhecido:'.$params.'. Os parâmetros válidos são: M, D, S, I e *.', 800); } $this->reader->consume(); $this->reader->consumeWhile(' '); $question->text = trim($this->reader->consumeUntil("\n", false, true)); if (strlen($question->text) === 0) { throw new Exception('Error na linha '.($this->line).'. Questões devem ter um texto associado!', 800); } if (!$this->reader->eof()) { $this->reader->consume(); $this->line++; } // echo $question->type; // echo '
'; if (in_array($question->type, array('M','S'))) { $this->parseChoices($question); if (count($question->choices) < 2) { throw new Exception('Error na linha '.($this->line).'. A questão '.$question->number.' é de múltipla escolha e deve ter pelo menos duas alternativas.', 805); } } return $question; } function parseChoices ($question) { $question->choices = array(); $letters = array(); while (!$this->reader->eof()) { $this->skipBlanklines(); if($this->reader->eof()){ // blank lines at the end of file continue; } $char = $this->reader->getChar(); $isPoint = $this->reader->lookFoward(1); if ($char === '#') { $this->reader->consumeUntil("\n", true); $this->line++; continue; } else if ($char === ':' || is_numeric($char)) { break; } else if ($isPoint !== '.') { // must be CONDICAO $cond = "CONDICAO:"; $actual = $this->reader->lookAhead(strlen($cond)); if(strcmp($cond, $actual) !== 0) { throw new Exception("Error na linha ".($this->line).". A expressão ".$actual." é desconhecida, o único valor válido nesse contexto seria a expressão 'CONDICAO:'.", 800); } // TODO parse conditionals $this->reader->consumeUntil("\n", true); $this->line++; break; } // echo 'Choice:'.$char; // echo '
'; if (in_array($char, $letters)) { throw new Exception('Error na linha '.($this->line).'. A letra '.$char.' já foi utilizada anteriormente nesta mesma questão.', 804); } else if (!preg_match('/[A-Z]/', $char)) { throw new Exception('Error na linha '.($this->line).'. As alternativas das questões devem ser letras maiúsculas A-Z.', 800); } else if ($isPoint !== '.') { throw new Exception('Error na linha '.($this->line).'. A letra da alternativa deve ser seguida de um .(ponto)!', 800); } $choice = new stdClass(); $choice->value = $char; $this->reader->consumeUntil(".", true); $this->reader->consumeWhile(' '); // text can be last line of valid text $choice->text = trim($this->reader->consumeUntil("\n", false, true)); if (strlen($choice->text) === 0) { throw new Exception('Error na linha '.($this->line).'. Alternativas devem ter um texto associado!', 800); } if(!$this->reader->eof()) { $this->reader->consume(); $this->line++; } $question->choices[] = $choice; } } }