KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

604 lines
20KB

  1. <?php
  2. /**
  3. * Twig::Parser
  4. * ~~~~~~~~~~~~
  5. *
  6. * This module implements the Twig parser.
  7. *
  8. * :copyright: 2008 by Armin Ronacher.
  9. * :license: BSD.
  10. */
  11. function twig_parse($source, $filename=NULL)
  12. {
  13. $stream = twig_tokenize($source, $filename);
  14. $parser = new Twig_Parser($stream);
  15. return $parser->parse();
  16. }
  17. class Twig_Parser
  18. {
  19. public $stream;
  20. public $blocks;
  21. public $extends;
  22. public $current_block;
  23. private $handlers;
  24. public function __construct($stream)
  25. {
  26. $this->stream = $stream;
  27. $this->extends = NULL;
  28. $this->blocks = array();
  29. $this->current_block = NULL;
  30. $this->handlers = array(
  31. 'for' => array($this, 'parseForLoop'),
  32. 'if' => array($this, 'parseIfCondition'),
  33. 'extends' => array($this, 'parseExtends'),
  34. 'include' => array($this, 'parseInclude'),
  35. 'block' => array($this, 'parseBlock'),
  36. 'super' => array($this, 'parseSuper'),
  37. # Chyrp specific extensions
  38. 'url' => array($this, 'parseURL'),
  39. 'admin' => array($this, 'parseAdminURL'),
  40. 'paginate' => array($this, 'parsePaginate')
  41. );
  42. }
  43. public function parseForLoop($token)
  44. {
  45. $lineno = $token->lineno;
  46. list($is_multitarget, $item) = $this->parseAssignmentExpression();
  47. $this->stream->expect('in');
  48. $seq = $this->parseExpression();
  49. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  50. $body = $this->subparse(array($this, 'decideForFork'));
  51. if ($this->stream->next()->value == 'else') {
  52. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  53. $else = $this->subparse(array($this, 'decideForEnd'), true);
  54. }
  55. else
  56. $else = NULL;
  57. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  58. return new Twig_ForLoop($is_multitarget, $item, $seq, $body, $else,
  59. $lineno);
  60. }
  61. public function parsePaginate($token)
  62. {
  63. $lineno = $token->lineno;
  64. $per_page = $this->parseExpression();
  65. $as = $this->parseExpression();
  66. $this->stream->expect('in');
  67. $loop = $this->parseExpression();
  68. $this->stream->expect('as');
  69. $item = $this->parseExpression();
  70. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  71. $body = $this->subparse(array($this, 'decidePaginateFork'));
  72. if ($this->stream->next()->value == 'else') {
  73. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  74. $else = $this->subparse(array($this, 'decidePaginateEnd'), true);
  75. }
  76. else
  77. $else = NULL;
  78. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  79. return new Twig_PaginateLoop($item, $per_page,
  80. $loop, $as, $body, $else, $lineno);
  81. }
  82. public function decideForFork($token)
  83. {
  84. return $token->test(array('else', 'endfor'));
  85. }
  86. public function decideForEnd($token)
  87. {
  88. return $token->test('endfor');
  89. }
  90. public function decidePaginateFork($token)
  91. {
  92. return $token->test(array('else', 'endpaginate'));
  93. }
  94. public function decidePaginateEnd($token)
  95. {
  96. return $token->test('endpaginate');
  97. }
  98. public function parseIfCondition($token)
  99. {
  100. $lineno = $token->lineno;
  101. $expr = $this->parseExpression();
  102. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  103. $body = $this->subparse(array($this, 'decideIfFork'));
  104. $tests = array(array($expr, $body));
  105. $else = NULL;
  106. $end = false;
  107. while (!$end)
  108. switch ($this->stream->next()->value) {
  109. case 'else':
  110. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  111. $else = $this->subparse(array($this, 'decideIfEnd'));
  112. break;
  113. case 'elseif':
  114. $expr = $this->parseExpression();
  115. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  116. $body = $this->subparse(array($this, 'decideIfFork'));
  117. $tests[] = array($expr, $body);
  118. break;
  119. case 'endif':
  120. $end = true;
  121. break;
  122. }
  123. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  124. return new Twig_IfCondition($tests, $else, $lineno);
  125. }
  126. public function decideIfFork($token)
  127. {
  128. return $token->test(array('elseif', 'else', 'endif'));
  129. }
  130. public function decideIfEnd($token)
  131. {
  132. return $token->test(array('endif'));
  133. }
  134. public function parseBlock($token)
  135. {
  136. $lineno = $token->lineno;
  137. $name = $this->stream->expect(Twig_Token::NAME_TYPE)->value;
  138. if (isset($this->blocks[$name]))
  139. throw new Twig_SyntaxError("block '$name' defined twice.",
  140. $lineno);
  141. $this->current_block = $name;
  142. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  143. $body = $this->subparse(array($this, 'decideBlockEnd'), true);
  144. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  145. $block = new Twig_Block($name, $body, $lineno);
  146. $this->blocks[$name] = $block;
  147. $this->current_block = NULL;
  148. return new Twig_BlockReference($name, $lineno);
  149. }
  150. public function decideBlockEnd($token)
  151. {
  152. return $token->test('endblock');
  153. }
  154. public function parseExtends($token)
  155. {
  156. $lineno = $token->lineno;
  157. if (!is_null($this->extends))
  158. throw new Twig_SyntaxError('multiple extend tags', $lineno);
  159. $this->extends = $this->stream->expect(Twig_Token::STRING_TYPE)->value;
  160. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  161. return NULL;
  162. }
  163. public function parseInclude($token)
  164. {
  165. $expr = $this->parseExpression();
  166. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  167. return new Twig_Include($expr, $token->lineno);
  168. }
  169. public function parseSuper($token)
  170. {
  171. if (is_null($this->current_block))
  172. throw new Twig_SyntaxError('super outside block', $token->lineno);
  173. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  174. return new Twig_Super($this->current_block, $token->lineno);
  175. }
  176. public function parseURL($token)
  177. {
  178. $expr = $this->parseExpression();
  179. if ($this->stream->test("in")) {
  180. $this->parseExpression();
  181. $cont = $this->parseExpression();
  182. } else
  183. $cont = null;
  184. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  185. return new Twig_URL($expr, $cont, $token->lineno);
  186. }
  187. public function parseAdminURL($token)
  188. {
  189. $expr = $this->parseExpression();
  190. $this->stream->expect(Twig_Token::BLOCK_END_TYPE);
  191. return new Twig_AdminURL($expr, $token->lineno);
  192. }
  193. public function parseExpression()
  194. {
  195. return $this->parseConditionalExpression();
  196. }
  197. public function parseConditionalExpression()
  198. {
  199. $lineno = $this->stream->current->lineno;
  200. $expr1 = $this->parseOrExpression();
  201. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '?')) {
  202. $this->stream->next();
  203. $expr2 = $this->parseOrExpression();
  204. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ':');
  205. $expr3 = $this->parseConditionalExpression();
  206. $expr1 = new Twig_ConditionalExpression($expr1, $expr2, $expr3,
  207. $this->lineno);
  208. $lineno = $this->stream->current->lineno;
  209. }
  210. return $expr1;
  211. }
  212. public function parseOrExpression()
  213. {
  214. $lineno = $this->stream->current->lineno;
  215. $left = $this->parseAndExpression();
  216. while ($this->stream->test('or')) {
  217. $this->stream->next();
  218. $right = $this->parseAndExpression();
  219. $left = new Twig_OrExpression($left, $right, $lineno);
  220. $lineno = $this->stream->current->lineno;
  221. }
  222. return $left;
  223. }
  224. public function parseAndExpression()
  225. {
  226. $lineno = $this->stream->current->lineno;
  227. $left = $this->parseCompareExpression();
  228. while ($this->stream->test('and')) {
  229. $this->stream->next();
  230. $right = $this->parseCompareExpression();
  231. $left = new Twig_AndExpression($left, $right, $lineno);
  232. $lineno = $this->stream->current->lineno;
  233. }
  234. return $left;
  235. }
  236. public function parseCompareExpression()
  237. {
  238. static $operators = array('==', '!=', '<', '>', '>=', '<=');
  239. $lineno = $this->stream->current->lineno;
  240. $expr = $this->parseAddExpression();
  241. $ops = array();
  242. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, $operators))
  243. $ops[] = array($this->stream->next()->value,
  244. $this->parseAddExpression());
  245. if (empty($ops))
  246. return $expr;
  247. return new Twig_CompareExpression($expr, $ops, $lineno);
  248. }
  249. public function parseAddExpression()
  250. {
  251. $lineno = $this->stream->current->lineno;
  252. $left = $this->parseSubExpression();
  253. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '+')) {
  254. $this->stream->next();
  255. $right = $this->parseSubExpression();
  256. $left = new Twig_AddExpression($left, $right, $lineno);
  257. $lineno = $this->stream->current->lineno;
  258. }
  259. return $left;
  260. }
  261. public function parseSubExpression()
  262. {
  263. $lineno = $this->stream->current->lineno;
  264. $left = $this->parseConcatExpression();
  265. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '-')) {
  266. $this->stream->next();
  267. $right = $this->parseConcatExpression();
  268. $left = new Twig_SubExpression($left, $right, $lineno);
  269. $lineno = $this->stream->current->lineno;
  270. }
  271. return $left;
  272. }
  273. public function parseConcatExpression()
  274. {
  275. $lineno = $this->stream->current->lineno;
  276. $left = $this->parseMulExpression();
  277. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '~')) {
  278. $this->stream->next();
  279. $right = $this->parseMulExpression();
  280. $left = new Twig_ConcatExpression($left, $right, $lineno);
  281. $lineno = $this->stream->current->lineno;
  282. }
  283. return $left;
  284. }
  285. public function parseMulExpression()
  286. {
  287. $lineno = $this->stream->current->lineno;
  288. $left = $this->parseDivExpression();
  289. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '*')) {
  290. $this->stream->next();
  291. $right = $this->parseDivExpression();
  292. $left = new Twig_MulExpression($left, $right, $lineno);
  293. $lineno = $this->stream->current->lineno;
  294. }
  295. return $left;
  296. }
  297. public function parseDivExpression()
  298. {
  299. $lineno = $this->stream->current->lineno;
  300. $left = $this->parseModExpression();
  301. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '/')) {
  302. $this->stream->next();
  303. $right = $this->parseModExpression();
  304. $left = new Twig_DivExpression($left, $right, $lineno);
  305. $lineno = $this->stream->current->lineno;
  306. }
  307. return $left;
  308. }
  309. public function parseModExpression()
  310. {
  311. $lineno = $this->stream->current->lineno;
  312. $left = $this->parseUnaryExpression();
  313. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '%')) {
  314. $this->stream->next();
  315. $right = $this->parseUnaryExpression();
  316. $left = new Twig_ModExpression($left, $right, $lineno);
  317. $lineno = $this->stream->current->lineno;
  318. }
  319. return $left;
  320. }
  321. public function parseUnaryExpression()
  322. {
  323. if ($this->stream->test('not'))
  324. return $this->parseNotExpression();
  325. if ($this->stream->current->type == Twig_Token::OPERATOR_TYPE) {
  326. switch ($this->stream->current->value) {
  327. case '-':
  328. return $this->parseNegExpression();
  329. case '+':
  330. return $this->parsePosExpression();
  331. }
  332. }
  333. return $this->parsePrimaryExpression();
  334. }
  335. public function parseNotExpression()
  336. {
  337. $token = $this->stream->next();
  338. $node = $this->parseUnaryExpression();
  339. return new Twig_NotExpression($node, $token->lineno);
  340. }
  341. public function parseNegExpression()
  342. {
  343. $token = $this->stream->next();
  344. $node = $this->parseUnaryExpression();
  345. return new Twig_NegExpression($node, $token->lineno);
  346. }
  347. public function parsePosExpression()
  348. {
  349. $token = $this->stream->next();
  350. $node = $this->parseUnaryExpression();
  351. return new Twig_PosExpression($node, $token->lineno);
  352. }
  353. public function parsePrimaryExpression($assignment=false)
  354. {
  355. $token = $this->stream->current;
  356. switch ($token->type) {
  357. case Twig_Token::NAME_TYPE:
  358. $this->stream->next();
  359. switch ($token->value) {
  360. case 'true':
  361. $node = new Twig_Constant(true, $token->lineno);
  362. break;
  363. case 'false':
  364. $node = new Twig_Constant(false, $token->lineno);
  365. break;
  366. case 'none':
  367. $node = new Twig_Constant(NULL, $token->lineno);
  368. break;
  369. default:
  370. $cls = $assignment ? 'Twig_AssignNameExpression'
  371. : 'Twig_NameExpression';
  372. $node = new $cls($token->value, $token->lineno);
  373. }
  374. break;
  375. case Twig_Token::NUMBER_TYPE:
  376. case Twig_Token::STRING_TYPE:
  377. $this->stream->next();
  378. $node = new Twig_Constant($token->value, $token->lineno);
  379. break;
  380. default:
  381. if ($token->test(Twig_Token::OPERATOR_TYPE, '(')) {
  382. $this->stream->next();
  383. $node = $this->parseExpression();
  384. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')');
  385. }
  386. else
  387. throw new Twig_SyntaxError('unexpected token',
  388. $token->lineno);
  389. }
  390. if (!$assignment)
  391. $node = $this->parsePostfixExpression($node);
  392. return $node;
  393. }
  394. public function parsePostfixExpression($node)
  395. {
  396. $stop = false;
  397. while (!$stop && $this->stream->current->type ==
  398. Twig_Token::OPERATOR_TYPE)
  399. switch ($this->stream->current->value) {
  400. case '.':
  401. case '[':
  402. $node = $this->parseSubscriptExpression($node);
  403. break;
  404. case '|':
  405. $node = $this->parseFilterExpression($node);
  406. break;
  407. default:
  408. $stop = true;
  409. break;
  410. }
  411. return $node;
  412. }
  413. public function parseSubscriptExpression($node)
  414. {
  415. $token = $this->stream->next();
  416. $lineno = $token->lineno;
  417. if ($token->value == '.') {
  418. $token = $this->stream->next();
  419. if ($token->type == Twig_Token::NAME_TYPE ||
  420. $token->type == Twig_Token::NUMBER_TYPE)
  421. $arg = new Twig_Constant($token->value, $lineno);
  422. else
  423. throw new Twig_SyntaxError('expected name or number',
  424. $lineno);
  425. }
  426. else {
  427. $arg = $this->parseExpression();
  428. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ']');
  429. }
  430. if (!$this->stream->test(Twig_Token::OPERATOR_TYPE, '('))
  431. return new Twig_GetAttrExpression($node, $arg, $lineno, $token->value);
  432. /* sounds like something wants to call a member with some
  433. arguments. Let's parse the parameters */
  434. $this->stream->next();
  435. $arguments = array();
  436. while (!$this->stream->test(Twig_Token::OPERATOR_TYPE, ')')) {
  437. if (count($arguments))
  438. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ',');
  439. $arguments[] = $this->parseExpression();
  440. }
  441. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')');
  442. return new Twig_MethodCallExpression($node, $arg, $arguments, $lineno);
  443. }
  444. public function parseFilterExpression($node)
  445. {
  446. $lineno = $this->stream->current->lineno;
  447. $filters = array();
  448. while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '|')) {
  449. $this->stream->next();
  450. $token = $this->stream->expect(Twig_Token::NAME_TYPE);
  451. $args = array();
  452. if ($this->stream->test(
  453. Twig_Token::OPERATOR_TYPE, '(')) {
  454. $this->stream->next();
  455. while (!$this->stream->test(
  456. Twig_Token::OPERATOR_TYPE, ')')) {
  457. if (!empty($args))
  458. $this->stream->expect(
  459. Twig_Token::OPERATOR_TYPE, ',');
  460. $args[] = $this->parseExpression();
  461. }
  462. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')');
  463. }
  464. $filters[] = array($token->value, $args);
  465. }
  466. return new Twig_FilterExpression($node, $filters, $lineno);
  467. }
  468. public function parseAssignmentExpression()
  469. {
  470. $lineno = $this->stream->current->lineno;
  471. $targets = array();
  472. $is_multitarget = false;
  473. while (true) {
  474. if (!empty($targets))
  475. $this->stream->expect(Twig_Token::OPERATOR_TYPE, ',');
  476. if ($this->stream->test(Twig_Token::OPERATOR_TYPE, ')') ||
  477. $this->stream->test(Twig_Token::VAR_END_TYPE) ||
  478. $this->stream->test(Twig_Token::BLOCK_END_TYPE) ||
  479. $this->stream->test('in'))
  480. break;
  481. $targets[] = $this->parsePrimaryExpression(true);
  482. if (!$this->stream->test(Twig_Token::OPERATOR_TYPE, ','))
  483. break;
  484. $is_multitarget = true;
  485. }
  486. if (!$is_multitarget && count($targets) == 1)
  487. return array(false, $targets[0]);
  488. return array(true, $targets);
  489. }
  490. public function subparse($test, $drop_needle=false)
  491. {
  492. $lineno = $this->stream->current->lineno;
  493. $rv = array();
  494. while (!$this->stream->eof) {
  495. switch ($this->stream->current->type) {
  496. case Twig_Token::TEXT_TYPE:
  497. $token = $this->stream->next();
  498. $rv[] = new Twig_Text($token->value, $token->lineno);
  499. break;
  500. case Twig_Token::VAR_START_TYPE:
  501. $token = $this->stream->next();
  502. $expr = $this->parseExpression();
  503. $this->stream->expect(Twig_Token::VAR_END_TYPE);
  504. $rv[] = new Twig_Print($expr, $token->lineno);
  505. break;
  506. case Twig_Token::BLOCK_START_TYPE:
  507. $this->stream->next();
  508. $token = $this->stream->current;
  509. if ($token->type !== Twig_Token::NAME_TYPE)
  510. throw new Twig_SyntaxError('expected directive',
  511. $token->lineno);
  512. if (!is_null($test) && call_user_func($test, $token)) {
  513. if ($drop_needle)
  514. $this->stream->next();
  515. return Twig_NodeList::fromArray($rv, $lineno);
  516. }
  517. if (!isset($this->handlers[$token->value]))
  518. throw new Twig_SyntaxError('unknown directive',
  519. $token->lineno);
  520. $this->stream->next();
  521. $node = call_user_func($this->handlers[$token->value],
  522. $token);
  523. if (!is_null($node))
  524. $rv[] = $node;
  525. break;
  526. default:
  527. assert(false, 'Lexer or parser ended up in ' .
  528. 'unsupported state.');
  529. }
  530. }
  531. return Twig_NodeList::fromArray($rv, $lineno);
  532. }
  533. public function parse()
  534. {
  535. try {
  536. $body = $this->subparse(NULL);
  537. }
  538. catch (Twig_SyntaxError $e) {
  539. if (is_null($e->filename))
  540. $e->filename = $this->stream->filename;
  541. throw $e;
  542. }
  543. if (!is_null($this->extends))
  544. foreach ($this->blocks as $block)
  545. $block->parent = $this->extends;
  546. return new Twig_Module($body, $this->extends, $this->blocks,
  547. $this->stream->filename);
  548. }
  549. }