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