formparser.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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. if($this->reader->eof()) break;
  104. $char = $this->reader->getChar();
  105. if ($char === '#') {
  106. // skip comment
  107. $this->reader->consumeUntil("\n", true);
  108. $this->line++;
  109. continue;
  110. }
  111. $char = $this->reader->consume();
  112. if ($char === ':') {
  113. $foward = $this->reader->getChar();
  114. if($foward !== ':') {
  115. $actual = $char.$foward;
  116. throw new Exception("Error na linha $this->line. ${actual} não é uma expressão válida, talvez você quisesse '::' (quebra de página )?", 800);
  117. }
  118. $this->reader->consumeUntil("\n", true);
  119. $this->line++;
  120. $pagebreak = new stdClass;
  121. $pagebreak->type = 'PAGEBREAK';
  122. $questions[] = $pagebreak;
  123. continue;
  124. }
  125. if (is_numeric($char)) {
  126. $foward = $this->reader->getChar();
  127. if (is_numeric($foward)) {
  128. // two digits
  129. $char = $char . $foward;
  130. $this->reader->consume();
  131. $foward = $this->reader->getChar();
  132. }
  133. if ($foward !== '.') {
  134. throw new Exception("Error na linha $this->line. O número da questão deve ser seguido de um .(ponto).", 800);
  135. }
  136. if (in_array($char, $numbers)) {
  137. throw new Exception('Error na linha '.($this->line).'. O número '.$char.' já foi utilizado anteriormente.', 802);
  138. }
  139. $numbers[] = $char;
  140. // read '.'
  141. $this->reader->consume();
  142. $questions[] = $this->parseQuestion($char);
  143. } else {
  144. throw new Exception('Error na linha '.($this->line).'. Você deve informar uma questão nessa linha', 800);
  145. }
  146. }
  147. } catch (Exception $e) {
  148. $message = $e->getMessage();
  149. // echo $e->getTraceAsString();
  150. throw new Exception ("Erro de processamento: $message Total de linhas processadas: $this->line", 800);
  151. }
  152. if (count($questions) == 0) {
  153. throw new Exception('Error na linha '.($this->line).'. Questionário deve ter ao menos uma questão.', 806);
  154. }
  155. return $questions;
  156. }
  157. function parseQuestion ($number) {
  158. $question = new stdClass();
  159. $question->number = 'q'.$number;
  160. // params
  161. $this->reader->consumeWhile(' ');
  162. if ($this->reader->getChar() !== '[') {
  163. throw new Exception('Error na linha '.($this->line).'. Toda questão deve ter seus parâmetros (M, D, S, I e *) definidos entre [ ].', 800);
  164. }
  165. $this->reader->consume();
  166. $params = $this->reader->consumeUntil("]");
  167. $params = str_replace(' ', '', $params);
  168. $validTypes = ['M','D','S', 'I'];
  169. if (strlen($params) === 0) {
  170. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
  171. }
  172. $found = false;
  173. foreach ($validTypes as $type) {
  174. if (strpos($params, $type) !== FALSE) {
  175. if ($found) {
  176. throw new Exception('Error na linha '.($this->line).'. Questões devem ter apenas um parâmetro de tipo: M, D, I ou S.', 803);
  177. }
  178. $question->type = $type;
  179. $found = true;
  180. }
  181. }
  182. if (!$found) {
  183. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um parâmetro de tipo: M, D, I ou S.', 803);
  184. }
  185. $params = str_replace($validTypes, '', $params);
  186. if(strlen($params) > 0) {
  187. if (strpos($params, '*') !== FALSE) {
  188. $question->required = true;
  189. $params = str_replace('*', '', $params);
  190. } else {
  191. 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);
  192. }
  193. }
  194. if(strlen($params) > 0) {
  195. 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);
  196. }
  197. $this->reader->consume();
  198. $this->reader->consumeWhile(' ');
  199. $question->text = trim($this->reader->consumeUntil("\n", false, true));
  200. if (strlen($question->text) === 0) {
  201. throw new Exception('Error na linha '.($this->line).'. Questões devem ter um texto associado!', 800);
  202. }
  203. if (!$this->reader->eof()) {
  204. $this->reader->consume();
  205. $this->line++;
  206. }
  207. // echo $question->type;
  208. // echo '<br/>';
  209. if (in_array($question->type, array('M','S'))) {
  210. $this->parseChoices($question);
  211. if (count($question->choices) < 2) {
  212. 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);
  213. }
  214. }
  215. return $question;
  216. }
  217. function parseChoices ($question) {
  218. $question->choices = array();
  219. $letters = array();
  220. while (!$this->reader->eof()) {
  221. $this->skipBlanklines();
  222. if($this->reader->eof()){
  223. // blank lines at the end of file
  224. continue;
  225. }
  226. $char = $this->reader->getChar();
  227. $isPoint = $this->reader->lookFoward(1);
  228. if ($char === '#') {
  229. $this->reader->consumeUntil("\n", true);
  230. $this->line++;
  231. continue;
  232. } else if ($char === ':' || is_numeric($char)) {
  233. break;
  234. } else if ($isPoint !== '.') {
  235. // must be CONDICAO
  236. $cond = "CONDICAO:";
  237. $actual = $this->reader->lookAhead(strlen($cond));
  238. if(strcmp($cond, $actual) !== 0) {
  239. 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);
  240. }
  241. // TODO parse conditionals
  242. $this->reader->consumeUntil("\n", true);
  243. $this->line++;
  244. break;
  245. }
  246. // echo 'Choice:'.$char;
  247. // echo '<br/>';
  248. if (in_array($char, $letters)) {
  249. throw new Exception('Error na linha '.($this->line).'. A letra '.$char.' já foi utilizada anteriormente nesta mesma questão.', 804);
  250. } else if (!preg_match('/[A-Z]/', $char)) {
  251. throw new Exception('Error na linha '.($this->line).'. As alternativas das questões devem ser letras maiúsculas A-Z.', 800);
  252. } else if ($isPoint !== '.') {
  253. throw new Exception('Error na linha '.($this->line).'. A letra da alternativa deve ser seguida de um .(ponto)!', 800);
  254. }
  255. $choice = new stdClass();
  256. $choice->value = $char;
  257. $this->reader->consumeUntil(".", true);
  258. $this->reader->consumeWhile(' ');
  259. // text can be last line of valid text
  260. $choice->text = trim($this->reader->consumeUntil("\n", false, true));
  261. if (strlen($choice->text) === 0) {
  262. throw new Exception('Error na linha '.($this->line).'. Alternativas devem ter um texto associado!', 800);
  263. }
  264. if(!$this->reader->eof()) {
  265. $this->reader->consume();
  266. $this->line++;
  267. }
  268. $question->choices[] = $choice;
  269. }
  270. }
  271. }