formparser.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <?php
  2. class SourceReader {
  3. private $source;
  4. private $pos = 0;
  5. private $size;
  6. function __construct ($source) {
  7. $this->source = str_replace(array("\r\n","\r"), "\n", $source);
  8. $this->size = strlen($this->source);
  9. }
  10. function remaining () {
  11. return $this->size - $this->pos;
  12. }
  13. function consume ($count = 1) {
  14. $val = '';
  15. for($i = 0; $i < $count; $i++) {
  16. if ($this->eof()) throw new Exception('End of file reached during read.', 801);
  17. $val = $val . $this->source[$this->pos];
  18. $this->pos++;
  19. }
  20. return $val;
  21. }
  22. function consumeUntil ($char, $inclusive = false, $stopAtEOF = false) {
  23. $skipped = '';
  24. while($this->source[$this->pos] !== $char) {
  25. if ($this->eof()) {
  26. if ($stopAtEOF) return $skipped;
  27. throw new Exception('End of file reached during read.', 801);
  28. }
  29. $skipped = $skipped . $this->source[$this->pos];
  30. $this->pos++;
  31. }
  32. if ($inclusive) {
  33. $skipped = $skipped . $char;
  34. $this->pos++;
  35. }
  36. return $skipped;
  37. }
  38. function consumeWhile ($char) {
  39. $skipped = '';
  40. while($this->source[$this->pos] === $char) {
  41. if ($this->eof()) throw new Exception('End of file reached during read.', 801);
  42. $skipped = $skipped . $this->source[$this->pos];
  43. $this->pos++;
  44. }
  45. return $skipped;
  46. }
  47. function eof () {
  48. return $this->pos >= $this->size;
  49. }
  50. function getChar () {
  51. return $this->source[$this->pos];
  52. }
  53. function lookAhead ($steps) {
  54. if (($this->pos + $steps) > $this->size) {
  55. throw new Exception('End of file reached during read.', 801);
  56. }
  57. $result = "";
  58. $i = $this->pos;
  59. for (; $i < ($this->pos + $steps); $i++) {
  60. $result = $result . $this->source[$i];
  61. }
  62. return $result;
  63. }
  64. function lookFoward ($pos) {
  65. if ($pos + $this->pos > $this->size) {
  66. throw new Exception('End of file reached during read.', 801);
  67. }
  68. return $this->source[$pos + $this->pos];
  69. }
  70. function lookBack ($steps) {
  71. if (($this->pos - $steps) < 0) {
  72. throw new Exception('End of file reached during read.', 801);
  73. }
  74. $result = "";
  75. $i = $this->pos;
  76. for (; $i > ($this->pos - $steps); $i--) {
  77. $result = $result . $this->source[$i];
  78. }
  79. return $result;
  80. }
  81. }
  82. class Parser {
  83. private $reader;
  84. private $line = 1;
  85. function __construct ($reader) {
  86. $this->reader = $reader;
  87. }
  88. function skipBlanklines () {
  89. while (in_array($this->reader->getChar(), array(" ", "\n"))) {
  90. $this->reader->consumeWhile(' ');
  91. if ($this->reader->getChar() === "\n") {
  92. $this->line++;
  93. $this->reader->consume();
  94. }
  95. }
  96. }
  97. function parse () {
  98. $numbers = array();
  99. $questions = array();
  100. try {
  101. while (!$this->reader->eof()) {
  102. $this->skipBlanklines();
  103. $char = $this->reader->getChar();
  104. if ($char === '#') {
  105. // skip comment
  106. $this->reader->consumeUntil("\n", true);
  107. $this->line++;
  108. continue;
  109. }
  110. $char = $this->reader->consume();
  111. if ($char === ':') {
  112. $foward = $this->reader->getChar();
  113. if($foward !== ':') {
  114. $actual = $char.$foward;
  115. throw new Exception("Error na linha $this->line. ${actual} não é uma expressão válida, talvez você quisesse '::' (quebra de página )?", 800);
  116. }
  117. $this->reader->consumeUntil("\n", true);
  118. $this->line++;
  119. $pagebreak = new stdClass;
  120. $pagebreak->type = 'PAGEBREAK';
  121. $questions[] = $pagebreak;
  122. continue;
  123. }
  124. if (is_numeric($char)) {
  125. $foward = $this->reader->getChar();
  126. if (is_numeric($foward)) {
  127. // two digits
  128. $char = $char . $foward;
  129. $this->reader->consume();
  130. $foward = $this->reader->getChar();
  131. }
  132. if ($foward !== '.') {
  133. throw new Exception("Error na linha $this->line. O número da questão deve ser seguido de um .(ponto).", 800);
  134. }
  135. if (in_array($char, $numbers)) {
  136. throw new Exception('Error na linha '.($this->line).'. O número '.$char.' já foi utilizado anteriormente.', 802);
  137. }
  138. $numbers[] = $char;
  139. // read '.'
  140. $this->reader->consume();
  141. $questions[] = $this->parseQuestion($char);
  142. } else {
  143. throw new Exception('Error na linha '.($this->line).'. Você deve informar uma questão nessa linha', 800);
  144. }
  145. }
  146. } catch (Exception $e) {
  147. $message = $e->getMessage();
  148. // echo $e->getTraceAsString();
  149. throw new Exception ("Erro de processamento: $message Total de linhas processadas: $this->line", 800);
  150. }
  151. if (count($questions) == 0) {
  152. throw new Exception('Error na linha '.($this->line).'. Questionário deve ter ao menos uma questão.', 806);
  153. }
  154. return $questions;
  155. }
  156. function parseQuestion ($number) {
  157. $question = new stdClass();
  158. $question->number = 'q'.$number;
  159. // params
  160. $this->reader->consumeWhile(' ');
  161. if ($this->reader->getChar() !== '[') {
  162. throw new Exception('Error na linha '.($this->line).'. Toda questão deve ter seus parâmetros (M, D, S, I e *) definidos entre [ ].', 800);
  163. }
  164. $this->reader->consume();
  165. $params = $this->reader->consumeUntil("]");
  166. $params = str_replace(' ', '', $params);
  167. $validTypes = ['M','D','S', 'I'];
  168. if (strlen($params) === 0) {
  169. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
  170. }
  171. $found = false;
  172. foreach ($validTypes as $type) {
  173. if (strpos($params, $type) !== FALSE) {
  174. if ($found) {
  175. throw new Exception('Error na linha '.($this->line).'. Questões devem ter apenas um parâmetro de tipo: M, D, I ou S.', 803);
  176. }
  177. $question->type = $type;
  178. $found = true;
  179. }
  180. }
  181. if (!$found) {
  182. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
  183. }
  184. $params = str_replace($validTypes, '', $params);
  185. if(strlen($params) > 0) {
  186. if (strpos($params, '*') !== FALSE) {
  187. $question->required = true;
  188. $params = str_replace('*', '', $params);
  189. } else {
  190. 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);
  191. }
  192. }
  193. if(strlen($params) > 0) {
  194. 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);
  195. }
  196. $this->reader->consume();
  197. $this->reader->consumeWhile(' ');
  198. $question->text = trim($this->reader->consumeUntil("\n"));
  199. if (strlen($question->text) === 0) {
  200. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um texto associado!', 800);
  201. }
  202. $this->reader->consume();
  203. $this->line++;
  204. // echo $question->type;
  205. // echo '<br/>';
  206. if (in_array($question->type, array('M','S'))) {
  207. $this->parseChoices($question);
  208. if (count($question->choices) < 2) {
  209. 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);
  210. }
  211. }
  212. return $question;
  213. }
  214. function parseChoices ($question) {
  215. $question->choices = array();
  216. $letters = array();
  217. while (!$this->reader->eof()) {
  218. $this->skipBlanklines();
  219. if($this->reader->eof()){
  220. // blank lines at the end of file
  221. continue;
  222. }
  223. $char = $this->reader->getChar();
  224. $isPoint = $this->reader->lookFoward(1);
  225. if ($char === '#') {
  226. $this->reader->consumeUntil("\n", true);
  227. $this->line++;
  228. continue;
  229. } else if ($char === ':' || is_numeric($char)) {
  230. break;
  231. } else if ($isPoint !== '.') {
  232. // must be CONDICAO
  233. $cond = "CONDICAO:";
  234. $actual = $this->reader->lookAhead(strlen($cond));
  235. if(strcmp($cond, $actual) !== 0) {
  236. 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);
  237. }
  238. // TODO parse conditionals
  239. $this->reader->consumeUntil("\n", true);
  240. $this->line++;
  241. break;
  242. }
  243. // echo 'Choice:'.$char;
  244. // echo '<br/>';
  245. if (in_array($char, $letters)) {
  246. throw new Exception('Error na linha '.($this->line).'. A letra '.$char.' já foi utilizada anteriormente nesta mesma questão.', 804);
  247. } else if (!preg_match('/[A-Z]/', $char)) {
  248. throw new Exception('Error na linha '.($this->line).'. As alternativas das questões devem ser letras maiúsculas A-Z.', 800);
  249. } else if ($isPoint !== '.') {
  250. throw new Exception('Error na linha '.($this->line).'. A letra da alternativa deve ser seguida de um .(ponto)!', 800);
  251. }
  252. $choice = new stdClass();
  253. $choice->value = $char;
  254. $this->reader->consumeUntil(".", true);
  255. $this->reader->consumeWhile(' ');
  256. // text can be last line of valid text
  257. $choice->text = trim($this->reader->consumeUntil("\n", false, true));
  258. if (strlen($choice->text) === 0) {
  259. throw new Exception('Error na linha '.($this->line).'. Alternativas devem ter um texto associado!', 800);
  260. }
  261. if(!$this->reader->eof()) {
  262. $this->reader->consume();
  263. $this->line++;
  264. }
  265. $question->choices[] = $choice;
  266. }
  267. }
  268. }