PageRenderTime 56ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/library/XenForo/Template/Compiler.php

https://github.com/hanguyenhuu/DTUI_201105
PHP | 1709 lines | 1027 code | 171 blank | 511 comment | 142 complexity | b9bc1c70769baef9fffb5a1100c0273d MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * General template compiling class. This takes a string (template) and converts it
  4. * into PHP code. This code represents the full statements.
  5. *
  6. * Most methods are public so as to be usable by the tag/function handlers. Externally,
  7. * {@link compile()} is the primary method to use for basic compilation.
  8. *
  9. * @package XenForo_Template
  10. */
  11. class XenForo_Template_Compiler
  12. {
  13. /**
  14. * Local cache parsed templates. Used for includes
  15. *
  16. * @var array
  17. */
  18. protected static $_templateCache = array();
  19. /**
  20. * The type of compiler. This should be unique per class, based on the source
  21. * for things like included templates, etc.
  22. *
  23. * @var string
  24. */
  25. protected static $_compilerType = 'public';
  26. /**
  27. * The text to compile
  28. *
  29. * @var string
  30. */
  31. protected $_text = '';
  32. /**
  33. * Array of objects that handle the named template tags. Key is tag name (lower case)
  34. * and value is the object.
  35. *
  36. * @var array
  37. */
  38. protected $_tagHandlers = array();
  39. /**
  40. * Array of objects that handle the named template functions. Key is the function
  41. * name (lower case) and value is the object.
  42. *
  43. * @var array
  44. */
  45. protected $_functionHandlers = array();
  46. /**
  47. * Default options for compilation. These will be used if individual handles do not
  48. * override them. Handlers may override all of them or individual ones.
  49. *
  50. * @var array
  51. */
  52. protected $_options = array(
  53. 'varEscape' => 'htmlspecialchars',
  54. 'allowRawStatements' => true,
  55. 'disableVarMap' => false
  56. );
  57. /**
  58. * Name of the variable that the should be used to create full statements
  59. *
  60. * @var string
  61. */
  62. protected $_outputVar = '__output';
  63. /**
  64. * Counter to create a unique variable name
  65. *
  66. * @var integer
  67. */
  68. protected $_uniqueVarCount = 0;
  69. /**
  70. * Prefix for a variable that holds internal content for the compiler.
  71. *
  72. * @var string
  73. */
  74. protected $_uniqueVarPrefix = '__compilerVar';
  75. /**
  76. * Controls whether external data (phrases, includes) should be followed
  77. * and inserted when compiling. This can be set to false for test compiles.
  78. *
  79. * @var boolean
  80. */
  81. protected $_followExternal = true;
  82. protected $_styleId = 0;
  83. protected $_languageId = 0;
  84. protected $_title = '';
  85. protected $_includedTemplates = array();
  86. protected static $_phraseCache = array();
  87. protected $_includedPhrases = array();
  88. protected $_enableDynamicPhraseLoad = true;
  89. /**
  90. * Line number currently on in the original version of the template.
  91. *
  92. * @var integer
  93. */
  94. protected $_lineNumber = 0;
  95. /**
  96. * Key value set of variables to map. This is primarily used for includes.
  97. * Key is the from, value is the to.
  98. *
  99. * @var array
  100. */
  101. protected $_variableMap = array();
  102. /**
  103. * Constructor. Sets up text.
  104. *
  105. * @param string Text to compile
  106. */
  107. public function __construct($text = '')
  108. {
  109. if ($text !== '')
  110. {
  111. $this->setText($text);
  112. }
  113. $this->_setupDefaults();
  114. }
  115. /**
  116. * Set up the defaults. Primarily sets up the handlers for various functions/tags.
  117. */
  118. protected function _setupDefaults()
  119. {
  120. $this->addFunctionHandlers(array(
  121. 'raw' => new XenForo_Template_Compiler_Function_Raw(),
  122. 'escape' => new XenForo_Template_Compiler_Function_Escape(),
  123. 'urlencode' => new XenForo_Template_Compiler_Function_UrlEncode(),
  124. 'jsescape' => new XenForo_Template_Compiler_Function_JsEscape(),
  125. 'phrase' => new XenForo_Template_Compiler_Function_Phrase(),
  126. 'property' => new XenForo_Template_Compiler_Function_Property(),
  127. 'pagenav' => new XenForo_Template_Compiler_Function_PageNav(),
  128. 'if' => new XenForo_Template_Compiler_Function_If(),
  129. 'checked' => new XenForo_Template_Compiler_Function_CheckedSelected(),
  130. 'selected' => new XenForo_Template_Compiler_Function_CheckedSelected(),
  131. 'date' => new XenForo_Template_Compiler_Function_DateTime(),
  132. 'time' => new XenForo_Template_Compiler_Function_DateTime(),
  133. 'datetime' => new XenForo_Template_Compiler_Function_DateTime(),
  134. 'number' => new XenForo_Template_Compiler_Function_Number(),
  135. 'link' => new XenForo_Template_Compiler_Function_Link(),
  136. 'adminlink' => new XenForo_Template_Compiler_Function_Link(),
  137. 'calc' => new XenForo_Template_Compiler_Function_Calc(),
  138. 'array' => new XenForo_Template_Compiler_Function_Array(),
  139. 'count' => new XenForo_Template_Compiler_Function_Count(),
  140. 'helper' => new XenForo_Template_Compiler_Function_Helper(),
  141. 'string' => new XenForo_Template_Compiler_Function_String(),
  142. ));
  143. $this->addTagHandlers(array(
  144. 'foreach' => new XenForo_Template_Compiler_Tag_Foreach(),
  145. 'if' => new XenForo_Template_Compiler_Tag_If(),
  146. 'elseif' => new XenForo_Template_Compiler_Tag_If(),
  147. 'else' => new XenForo_Template_Compiler_Tag_If(),
  148. 'contentcheck' => new XenForo_Template_Compiler_Tag_If(),
  149. 'navigation' => new XenForo_Template_Compiler_Tag_Navigation(),
  150. 'breadcrumb' => new XenForo_Template_Compiler_Tag_Navigation(),
  151. 'title' => new XenForo_Template_Compiler_Tag_Title(),
  152. 'description' => new XenForo_Template_Compiler_Tag_Description(),
  153. 'h1' => new XenForo_Template_Compiler_Tag_H1(),
  154. 'sidebar' => new XenForo_Template_Compiler_Tag_Sidebar(),
  155. 'topctrl' => new XenForo_Template_Compiler_Tag_TopCtrl(),
  156. 'container' => new XenForo_Template_Compiler_Tag_Container(),
  157. 'require' => new XenForo_Template_Compiler_Tag_Require(),
  158. 'include' => new XenForo_Template_Compiler_Tag_Include(),
  159. 'edithint' => new XenForo_Template_Compiler_Tag_EditHint(),
  160. 'set' => new XenForo_Template_Compiler_Tag_Set(),
  161. 'hook' => new XenForo_Template_Compiler_Tag_Hook(),
  162. 'formaction' => new XenForo_Template_Compiler_Tag_FormAction(),
  163. 'datetime' => new XenForo_Template_Compiler_Tag_DateTime(),
  164. 'avatar' => new XenForo_Template_Compiler_Tag_Avatar(),
  165. 'username' => new XenForo_Template_Compiler_Tag_Username(),
  166. 'likes' => new XenForo_Template_Compiler_Tag_Likes(),
  167. 'follow' => new XenForo_Template_Compiler_Tag_Follow(),
  168. 'pagenav' => new XenForo_Template_Compiler_Tag_PageNav(),
  169. // note: comment and untreated are handled by the lexer/parser
  170. ));
  171. }
  172. /**
  173. * Modifies the default options. Note that this merges into the options, maintaining
  174. * any that are not specified in the parameter.
  175. *
  176. * @param array
  177. *
  178. * @return XenForo_Template_Compiler Fluent interface ($this)
  179. */
  180. public function setDefaultOptions(array $options)
  181. {
  182. $this->_options = array_merge($this->_options, $options);
  183. return $this;
  184. }
  185. /**
  186. * Gets the current set of default options.
  187. *
  188. * @return array
  189. */
  190. public function getDefaultOptions()
  191. {
  192. return $this->_options;
  193. }
  194. /**
  195. * Sets the text to be compiled.
  196. *
  197. * @param string
  198. */
  199. public function setText($text)
  200. {
  201. $this->_text = strval($text);
  202. }
  203. /**
  204. * Adds or replaces a template tag handler.
  205. *
  206. * @param string Name of tag to handle
  207. * @param XenForo_Template_Compiler_Tag_Interface Handler object
  208. *
  209. * @return XenForo_Template_Compiler Fluent interface ($this)
  210. */
  211. public function addTagHandler($tag, XenForo_Template_Compiler_Tag_Interface $handler)
  212. {
  213. $this->_tagHandlers[strtolower($tag)] = $handler;
  214. return $this;
  215. }
  216. /**
  217. * Adds or replaces an array of template tag handlers.
  218. *
  219. * @param array Tag handlers; key: tag name, value: object
  220. *
  221. * @return XenForo_Template_Compiler Fluent interface ($this)
  222. */
  223. public function addTagHandlers(array $tags)
  224. {
  225. foreach ($tags AS $tag => $handler)
  226. {
  227. $this->addTagHandler($tag, $handler);
  228. }
  229. return $this;
  230. }
  231. /**
  232. * Adds or replaces a template function handler.
  233. *
  234. * @param string Name of function to handle
  235. * @param XenForo_Template_Compiler_Function_Interface Handler object
  236. *
  237. * @return XenForo_Template_Compiler Fluent interface ($this)
  238. */
  239. public function addFunctionHandler($function, XenForo_Template_Compiler_Function_Interface $handler)
  240. {
  241. $this->_functionHandlers[strtolower($function)] = $handler;
  242. return $this;
  243. }
  244. /**
  245. * Adds or replaces an array of template function handlers.
  246. *
  247. * @param array Function handlers; key: function name, value: object
  248. *
  249. * @return XenForo_Template_Compiler Fluent interface ($this)
  250. */
  251. public function addFunctionHandlers(array $functions)
  252. {
  253. foreach ($functions AS $function => $handler)
  254. {
  255. $this->addFunctionHandler($function, $handler);
  256. }
  257. return $this;
  258. }
  259. /**
  260. * Gets the variable name that full statements will write their contents into.
  261. *
  262. * @return string
  263. */
  264. public function getOutputVar()
  265. {
  266. return $this->_outputVar;
  267. }
  268. /**
  269. * Sets the variable name that full statements will write their contents into.
  270. *
  271. * @param string
  272. */
  273. public function setOutputVar($_outputVar)
  274. {
  275. $this->_outputVar = strval($_outputVar);
  276. }
  277. /**
  278. * Gets a unique variable name for an internal variable.
  279. *
  280. * @return string
  281. */
  282. public function getUniqueVar()
  283. {
  284. return $this->_uniqueVarPrefix . ++$this->_uniqueVarCount;
  285. }
  286. /**
  287. * Compiles this template from a string. Returns any number of statements.
  288. *
  289. * @param string $title Title of this template (required to prevent circular references)
  290. * @param integer $styleId Style ID this template belongs to (for template includes)
  291. * @param integer $languageId Language ID this compilation is for (used for phrases)
  292. *
  293. * @return string
  294. */
  295. public function compile($title = '', $styleId = 0, $languageId = 0)
  296. {
  297. $segments = $this->lexAndParse();
  298. return $this->compileParsed($segments, $title, $styleId, $languageId);
  299. }
  300. /**
  301. * Compiles this template from its parsed output.
  302. *
  303. * @param string|array $segments
  304. * @param string $title Title of this template (required to prevent circular references)
  305. * @param integer $styleId Style ID this template belongs to (for template includes)
  306. * @param integer $languageId Language ID this compilation is for (used for phrases)
  307. *
  308. * @return string
  309. */
  310. public function compileParsed($segments, $title, $styleId, $languageId)
  311. {
  312. $this->_title = $title;
  313. $this->_styleId = $styleId;
  314. $this->_languageId = $languageId;
  315. $this->_includedTemplates = array();
  316. if (!is_string($segments) && !is_array($segments))
  317. {
  318. throw new XenForo_Exception('Got unexpected, non-string/non-array segments for compilation.');
  319. }
  320. $this->_findAndLoadPhrasesFromSegments($segments);
  321. $statements = $this->compileSegments($segments);
  322. return $this->getOutputVarInitializer() . $statements->getFullStatements($this->_outputVar);
  323. }
  324. /**
  325. * Compiles this template from its parsed output. The template is considered to be plain
  326. * text (the default variable escaping is disabled).
  327. *
  328. * @param string|array $segments
  329. * @param string $title Title of this template (required to prevent circular references)
  330. * @param integer $styleId Style ID this template belongs to (for template includes)
  331. * @param integer $languageId Language ID this compilation is for (used for phrases)
  332. *
  333. * @return string
  334. */
  335. public function compileParsedPlainText($segments, $title, $styleId, $languageId)
  336. {
  337. $existingOptions = $this->getDefaultOptions();
  338. $this->setDefaultOptions(array('varEscape' => false));
  339. $compiled = $this->compileParsed($segments, $title, $styleId, $languageId);
  340. $this->setDefaultOptions($existingOptions);
  341. return $compiled;
  342. }
  343. /**
  344. * Helper funcion to compile the provided segments into the specified variable.
  345. * This is commonly used to simplify compilation of data that needs to be passed
  346. * into a function (eg, the children of a form tag).
  347. *
  348. * @param string|array $segments Segmenets
  349. * @param string $var Name of the variable to compile into. If generateVar is true, this will be written to (by ref).
  350. * @param array $options Compiler options
  351. * @param boolean $generateVar Whether to generate the var in argument 2 or use the provided input
  352. *
  353. * @return string Full compiled statements
  354. */
  355. public function compileIntoVariable($segments, &$var = '', array $options = null, $generateVar = true)
  356. {
  357. if ($generateVar)
  358. {
  359. $var = $this->getUniqueVar();
  360. }
  361. $oldOutputVar = $this->getOutputVar();
  362. $this->setOutputVar($var);
  363. $output =
  364. $this->getOutputVarInitializer()
  365. . $this->compileSegments($segments, $options)->getFullStatements($var);
  366. $this->setOutputVar($oldOutputVar);
  367. return $output;
  368. }
  369. /**
  370. * Gets the PHP statement that initializers the output var.
  371. *
  372. * @return string
  373. */
  374. public function getOutputVarInitializer()
  375. {
  376. return '$' . $this->_outputVar . " = '';\n";
  377. }
  378. /**
  379. * Combine uncompiled segments into a string of PHP code. This is simply a helper
  380. * function that compiles and then combines them for you.
  381. *
  382. * @param string|array Segment(s)
  383. * @param array|null Override options. If specified, this represents all options.
  384. *
  385. * @return string Valid PHP code
  386. */
  387. public function compileAndCombineSegments($segments, array $options = null)
  388. {
  389. if (!is_array($options))
  390. {
  391. $options = $this->_options;
  392. }
  393. $options = array_merge($options, array('allowRawStatements' => false));
  394. return $this->compileSegments($segments, $options)->getPartialStatement();
  395. }
  396. /**
  397. * Lex and parse the template into segments for final compilation.
  398. *
  399. * @return array Parsed segments
  400. */
  401. public function lexAndParse()
  402. {
  403. $lexer = new XenForo_Template_Compiler_Lexer($this->_text);
  404. $parser = new XenForo_Template_Compiler_Parser();
  405. try
  406. {
  407. while ($lexer->yylex() !== false)
  408. {
  409. $parser->doParse($lexer->match[0], $lexer->match[1]);
  410. $parser->setLineNumber($lexer->line); // if this is before the doParse, it seems to give wrong numbers
  411. }
  412. $parser->doParse(0, 0);
  413. }
  414. catch (Exception $e)
  415. {
  416. // from lexer, can't use the base exception, re-throw
  417. throw new XenForo_Template_Compiler_Exception(new XenForo_Phrase('line_x_template_syntax_error', array('number' => $lexer->line)), true);
  418. }
  419. // XenForo_Template_Compiler_Exception: ok -- no need to catch and rethrow
  420. return $parser->getOutput();
  421. }
  422. /**
  423. * Compile segments into an array of PHP code.
  424. *
  425. * @param string|array Segment(s)
  426. * @param array|null Override options. If specified, this represents all options.
  427. *
  428. * @return XenForo_Template_Compiler_Statement_Collection Collection of parts of a statement or sub statements
  429. */
  430. public function compileSegments($segments, array $options = null)
  431. {
  432. $segments = $this->prepareSegmentsForIteration($segments);
  433. if (!is_array($options))
  434. {
  435. $options = $this->_options;
  436. }
  437. $statement = $this->getNewStatementCollection();
  438. foreach ($segments AS $segment)
  439. {
  440. $compiled = $this->compileSegment($segment, $options);
  441. if ($compiled !== '' && $compiled !== null)
  442. {
  443. $statement->addStatement($compiled);
  444. }
  445. }
  446. return $statement;
  447. }
  448. /**
  449. * Prepare a collection of segments for iteration. This sanitizes the segments
  450. * so that each step will give you the next segment, which itself may be a string
  451. * or an array.
  452. *
  453. * @param string|array
  454. *
  455. * @return array
  456. */
  457. public function prepareSegmentsForIteration($segments)
  458. {
  459. if (!is_array($segments))
  460. {
  461. // likely a string (simple literal)
  462. $segments = array($segments);
  463. }
  464. else if (isset($segments['type']))
  465. {
  466. // a simple curly var/function
  467. $segments = array($segments);
  468. }
  469. return $segments;
  470. }
  471. /**
  472. * Compile segment into PHP code
  473. *
  474. * @param string|array Segment
  475. * @param array Override options, must be specified
  476. *
  477. * @return string
  478. */
  479. public function compileSegment($segment, array $options)
  480. {
  481. if (is_string($segment))
  482. {
  483. $this->setLastVistedSegment($segment);
  484. return $this->compilePlainText($segment, $options);
  485. }
  486. else if (is_array($segment) && isset($segment['type']))
  487. {
  488. $this->setLastVistedSegment($segment);
  489. switch ($segment['type'])
  490. {
  491. case 'TAG':
  492. return $this->compileTag(
  493. $segment['name'], $segment['attributes'],
  494. isset($segment['children']) ? $segment['children'] : array(),
  495. $options
  496. );
  497. case 'CURLY_VAR':
  498. return $this->compileVar($segment['name'], $segment['keys'], $options);
  499. case 'CURLY_FUNCTION':
  500. return $this->compileFunction($segment['name'], $segment['arguments'], $options);
  501. }
  502. }
  503. else if ($segment === null)
  504. {
  505. return '';
  506. }
  507. throw $this->getNewCompilerException(new XenForo_Phrase('internal_compiler_error_unknown_segment_type'));
  508. }
  509. /**
  510. * Sets the last segment that has been visited, updating the line number
  511. * to reflect this.
  512. *
  513. * @param mixed $segment
  514. */
  515. public function setLastVistedSegment($segment)
  516. {
  517. if (is_array($segment) && isset($segment['type']))
  518. {
  519. if (!empty($segment['line']))
  520. {
  521. $this->_lineNumber = $segment['line'];
  522. }
  523. }
  524. }
  525. /**
  526. * Escape a string for use inside a single-quoted string.
  527. *
  528. * @param string
  529. *
  530. * @return string
  531. */
  532. public function escapeSingleQuotedString($string)
  533. {
  534. return str_replace(array('\\', "'"), array('\\\\', "\'"), $string);
  535. }
  536. /**
  537. * Compile a plain text segment.
  538. *
  539. * @param string Text to compile
  540. * @param array Options
  541. */
  542. public function compilePlainText($text, array $options)
  543. {
  544. return "'" . $this->escapeSingleQuotedString($text) . "'";
  545. }
  546. /**
  547. * Compile a tag segment. Mostly handled by the specified tag handler.
  548. *
  549. * @param string Tag found
  550. * @param array Attributes (key: name, value: value)
  551. * @param array Any nodes (text, var, tag) that are within this tag
  552. * @param array Options
  553. */
  554. public function compileTag($tag, array $attributes, array $children, array $options)
  555. {
  556. $tag = strtolower($tag);
  557. if (isset($this->_tagHandlers[$tag]))
  558. {
  559. return $this->_tagHandlers[$tag]->compile($this, $tag, $attributes, $children, $options);
  560. }
  561. else
  562. {
  563. throw $this->getNewCompilerException(new XenForo_Phrase('unknown_tag_x', array('tag' => $tag)));
  564. }
  565. }
  566. /**
  567. * Compile a var segment.
  568. *
  569. * @param string Name of variable found, not including keys
  570. * @param array Keys, may be empty
  571. * @param array Options
  572. */
  573. public function compileVar($name, $keys, array $options)
  574. {
  575. $name = $this->resolveMappedVariable($name, $options);
  576. $varName = '$' . $name;
  577. if (!empty($keys) && is_array($keys))
  578. {
  579. foreach ($keys AS $key)
  580. {
  581. if (is_string($key))
  582. {
  583. $varName .= "['" . $this->escapeSingleQuotedString($key) . "']";
  584. }
  585. else if (isset($key['type']) && $key['type'] == 'CURLY_VAR')
  586. {
  587. $varName .= '[' . $this->compileVar($key['name'], $key['keys'], array_merge($options, array('varEscape' => false))) . ']';
  588. }
  589. }
  590. }
  591. if (!empty($options['varEscape']))
  592. {
  593. return $options['varEscape'] . '(' . $varName . ')';
  594. }
  595. else
  596. {
  597. return $varName;
  598. }
  599. }
  600. /**
  601. * Compile a function segment.
  602. *
  603. * @param string Name of function found
  604. * @param array Arguments (really should have at least 1 value). Each argument may be any number of segments
  605. * @param array Options
  606. */
  607. public function compileFunction($function, array $arguments, array $options)
  608. {
  609. $function = strtolower($function);
  610. if (isset($this->_functionHandlers[$function]))
  611. {
  612. return $this->_functionHandlers[$function]->compile($this, $function, $arguments, $options);
  613. }
  614. else
  615. {
  616. throw $this->getNewCompilerException(new XenForo_Phrase('unknown_function_x', array('function' => $function)));
  617. }
  618. }
  619. /**
  620. * Compiles a variable reference. A var ref is a string that looks somewhat like a variable.
  621. * It is used in some arguments to simplify variable access and only allow variables.
  622. *
  623. * Data received is any number of segments containing strings or variable segments.
  624. *
  625. * Examples: $var, $var.key, $var.{$key}.2, {$key}, {$key}.blah, {$key.blah}.x
  626. *
  627. * @param string|array Variable reference segment(s)
  628. * @param array Options
  629. *
  630. * @return string PHP code to access named variable
  631. */
  632. public function compileVarRef($varRef, array $options)
  633. {
  634. $replacements = array();
  635. if (is_array($varRef))
  636. {
  637. if (!isset($varRef[0]))
  638. {
  639. $varRef = array($varRef);
  640. }
  641. $newVarRef = '';
  642. foreach ($varRef AS $segment)
  643. {
  644. if (is_string($segment))
  645. {
  646. $newVarRef .= $segment;
  647. }
  648. else
  649. {
  650. $newVarRef .= '?';
  651. $replacements[] = $segment;
  652. }
  653. }
  654. $varRef = $newVarRef;
  655. }
  656. $parts = explode('.', $varRef);
  657. $variable = array_shift($parts);
  658. if ($variable == '?')
  659. {
  660. $variable = $this->compileSegment(array_shift($replacements), array_merge($options, array('varEscape' => false)));
  661. if (!preg_match('#^\$[a-zA-Z_]#', $variable))
  662. {
  663. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
  664. }
  665. }
  666. else if (!preg_match('#^\$([a-zA-Z_][a-zA-Z0-9_]*)$#', $variable))
  667. {
  668. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
  669. }
  670. $keys = array();
  671. foreach ($parts AS $part)
  672. {
  673. if ($part == '?')
  674. {
  675. $part = $this->compileSegment(array_shift($replacements), array_merge($options, array('varEscape' => false)));
  676. }
  677. else if ($part === '' || strpos($part, '?') !== false)
  678. {
  679. // empty key or simply contains a replacement
  680. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
  681. }
  682. else
  683. {
  684. $part = "'" . $this->escapeSingleQuotedString($part) . "'";
  685. }
  686. $keys[] = '[' . $part . ']';
  687. }
  688. $variable = '$' . $this->resolveMappedVariable(substr($variable, 1), $options);
  689. return $variable . implode('', $keys);
  690. }
  691. /**
  692. * Parses a set of named arguments. Each argument should be in the form of "key=value".
  693. * The key must be literal, but the value can be anything.
  694. *
  695. * @param array Arguments to treat as named
  696. *
  697. * @return array Key is the argument name, value is segment(s) to be compiled
  698. */
  699. function parseNamedArguments(array $arguments)
  700. {
  701. $params = array();
  702. foreach ($arguments AS $argument)
  703. {
  704. if (!isset($argument[0]) || !is_string($argument[0]) || !preg_match('#^([a-z0-9_\.]+)=#i', $argument[0], $match))
  705. {
  706. throw $this->getNewCompilerException(new XenForo_Phrase('named_parameter_not_specified_correctly'));
  707. }
  708. $name = $match[1];
  709. $nameRemoved = substr($argument[0], strlen($match[0]));
  710. if ($nameRemoved === false)
  711. {
  712. // we ate the whole string, remove the argument
  713. unset($argument[0]);
  714. }
  715. else
  716. {
  717. $argument[0] = $nameRemoved;
  718. }
  719. $nameParts = explode('.', $name);
  720. if (count($nameParts) > 1)
  721. {
  722. $pointer =& $params;
  723. foreach ($nameParts AS $namePart)
  724. {
  725. if (!isset($pointer[$namePart]))
  726. {
  727. $pointer[$namePart] = array();
  728. }
  729. $pointer =& $pointer[$namePart];
  730. }
  731. $pointer = $argument;
  732. }
  733. else
  734. {
  735. $params[$name] = $argument;
  736. }
  737. }
  738. return $params;
  739. }
  740. /**
  741. * Compiled a set of named params into a set of named params that can be used as PHP code.
  742. * The key is a single quoted string.
  743. *
  744. * @param array See {@link parseNamedArguments()}. Key is the name, value is segments for that param.
  745. * @param array Compiler options
  746. * @param array A list of named params should be compiled as conditions instead of plain output
  747. *
  748. * @return array
  749. */
  750. public function compileNamedParams(array $params, array $options, array $compileAsCondition = array())
  751. {
  752. $compiled = array();
  753. foreach ($params AS $name => $value)
  754. {
  755. if (in_array($name, $compileAsCondition))
  756. {
  757. $compiled[$name] = $this->parseConditionExpression($value, $options);
  758. }
  759. else
  760. {
  761. if (is_array($value))
  762. {
  763. // if an associative array, not a list of segments
  764. reset($value);
  765. list($key, ) = each($value);
  766. if (is_string($key))
  767. {
  768. $compiled[$name] = $this->compileNamedParams($value, $options);
  769. continue;
  770. }
  771. }
  772. $compiled[$name] = $this->compileAndCombineSegments($value, $options);
  773. }
  774. }
  775. return $compiled;
  776. }
  777. /**
  778. * Build actual PHP code from a set of compiled named params
  779. *
  780. * @param array $compiled Already compiled named params. See {@link compileNamedParams}.
  781. *
  782. * @return string
  783. */
  784. public function buildNamedParamCode(array $compiled)
  785. {
  786. if (!$compiled)
  787. {
  788. return 'array()';
  789. }
  790. $output = "array(\n";
  791. $i = 0;
  792. foreach ($compiled AS $name => $value)
  793. {
  794. if (is_array($value))
  795. {
  796. $value = $this->buildNamedParamCode($value);
  797. }
  798. if ($i > 0)
  799. {
  800. $output .= ",\n";
  801. }
  802. $output .= "'" . $this->escapeSingleQuotedString($name) . "' => $value";
  803. $i++;
  804. }
  805. $output .= "\n)";
  806. return $output;
  807. }
  808. /**
  809. * Takes a compiled set of named parameters and turns them into PHP code (an array).
  810. *
  811. * @param array See {@link parseNamedArguments()}. Key is the name, value is segments for that param.
  812. * @param array Compiler options
  813. * @param array A list of named params should be compiled as conditions instead of plain output
  814. *
  815. * @return string PHP code for an array
  816. */
  817. public function getNamedParamsAsPhpCode(array $params, array $options, array $compileAsCondition = array())
  818. {
  819. $compiled = $this->compileNamedParams($params, $options, $compileAsCondition);
  820. return $this->buildNamedParamCode($compiled);
  821. }
  822. /**
  823. * Creates a new raw statement handler.
  824. *
  825. * @param string Quickly set a statement
  826. *
  827. * @return XenForo_Template_Compiler_Statement_Raw
  828. */
  829. public function getNewRawStatement($statement = '')
  830. {
  831. return new XenForo_Template_Compiler_Statement_Raw($statement);
  832. }
  833. /**
  834. * Creates a new statement collection handler.
  835. *
  836. * @return XenForo_Template_Compiler_Statement_Collection
  837. */
  838. public function getNewStatementCollection()
  839. {
  840. return new XenForo_Template_Compiler_Statement_Collection();
  841. }
  842. /**
  843. * Creates a new compiler exception.
  844. *
  845. * @param string $message Optional message
  846. * @param mixed $segment The segment that caused this. If specified and has a line number, that line is reported.
  847. *
  848. * @return XenForo_Template_Compiler_Exception
  849. */
  850. public function getNewCompilerException($message = '', $segment = false)
  851. {
  852. if (is_array($segment) && !empty($segment['line']))
  853. {
  854. $lineNumber = $segment['line'];
  855. }
  856. else if (is_int($segment) && !empty($segment))
  857. {
  858. $lineNumber = $segment;
  859. }
  860. else
  861. {
  862. $lineNumber = $this->_lineNumber;
  863. }
  864. if ($lineNumber)
  865. {
  866. $message = new XenForo_Phrase('line_x', array('line' => $lineNumber)) . ': ' . $message;
  867. }
  868. $e = new XenForo_Template_Compiler_Exception($message, true);
  869. $e->setLineNumber($lineNumber);
  870. return $e;
  871. }
  872. /**
  873. * Creates a new compiler exception for an incorrect amount of arguments.
  874. *
  875. * @param mixed $segment The segment that caused this. If specified and has a line number, that line is reported.
  876. *
  877. * @return XenForo_Template_Compiler_Exception
  878. */
  879. public function getNewCompilerArgumentException($segment = false)
  880. {
  881. return $this->getNewCompilerException(new XenForo_Phrase('incorrect_arguments'), $segment);
  882. }
  883. /**
  884. * Determines if the segment is a tag with the specified name.
  885. *
  886. * @param string|array Segment
  887. * @param string Tag name
  888. *
  889. * @return boolean
  890. */
  891. public function isSegmentNamedTag($segment, $tagName)
  892. {
  893. return (is_array($segment) && isset($segment['type']) && $segment['type'] == 'TAG' && $segment['name'] == $tagName);
  894. }
  895. /**
  896. * Parses a conditional expression into valid PHP code
  897. *
  898. * @param string|array The original unparsed condition. This will consist of plaintext or curly var/function segments.
  899. * @param array Compiler options
  900. *
  901. * @return string Valid PHP code for the condition
  902. */
  903. public function parseConditionExpression($origCondition, array $options)
  904. {
  905. $placeholders = array();
  906. $placeholderChar = "\x1A"; // substitute character in ascii
  907. if ($origCondition === '')
  908. {
  909. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
  910. }
  911. if (is_string($origCondition))
  912. {
  913. $condition = $origCondition;
  914. }
  915. else
  916. {
  917. $condition = '';
  918. foreach ($this->prepareSegmentsForIteration($origCondition) AS $segment)
  919. {
  920. if (is_string($segment))
  921. {
  922. if (strpos($segment, $placeholderChar) !== false)
  923. {
  924. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
  925. }
  926. $condition .= $segment;
  927. }
  928. else
  929. {
  930. $condition .= $placeholderChar;
  931. $placeholders[] = $this->compileSegment($segment, array_merge($options, array('varEscape' => false)));
  932. }
  933. }
  934. }
  935. return $this->_parseConditionExpression($condition, $placeholders);
  936. }
  937. /**
  938. * Internal function for parsing a condition expression. Note that the variables
  939. * passed into this will be modified by reference.
  940. *
  941. * @param string Expression with placeholders replaced with "\x1A"
  942. * @param array Placeholders for variables/functions
  943. * @param boolean Whether to return when we match a right parenthesis
  944. *
  945. * @return string Parsed condition
  946. */
  947. protected function _parseConditionExpression(&$expression, array &$placeholders,
  948. $internalExpression = false, $isFunction = false)
  949. {
  950. if ($internalExpression && $isFunction && strlen($expression) > 0 && $expression[0] == ')')
  951. {
  952. $expression = substr($expression, 1);
  953. return '()';
  954. }
  955. $state = 'value';
  956. $endState = 'operator';
  957. $compiled = '';
  958. $allowedFunctions = 'is_array|is_object|is_string|isset|empty'
  959. . '|array|array_key_exists|count|in_array|array_search'
  960. . '|preg_match|preg_match_all|strpos|stripos|strlen'
  961. . '|ceil|floor|round|max|min|mt_rand|rand';
  962. do
  963. {
  964. $eatChars = 0;
  965. if ($state == 'value')
  966. {
  967. if (preg_match('#^\s+#', $expression, $match))
  968. {
  969. // ignore whitespace
  970. $eatChars = strlen($match[0]);
  971. }
  972. else if ($expression[0] == "\x1A")
  973. {
  974. $compiled .= array_shift($placeholders);
  975. $state = 'operator';
  976. $eatChars = 1;
  977. }
  978. else if ($expression[0] == '(')
  979. {
  980. $expression = substr($expression, 1);
  981. $compiled .= $this->_parseConditionExpression($expression, $placeholders, true);
  982. $state = 'operator';
  983. continue; // not eating anything, so must continue
  984. }
  985. else if (preg_match('#^(\-|!)#', $expression, $match))
  986. {
  987. $compiled .= $match[0];
  988. $state = 'value'; // we still need a value after this, simply modifies the following value
  989. $eatChars = strlen($match[0]);
  990. }
  991. else if (preg_match('#^(\d+(\.\d+)?|true|false|null)#', $expression, $match))
  992. {
  993. $compiled .= $match[0];
  994. $state = 'operator';
  995. $eatChars = strlen($match[0]);
  996. }
  997. else if (preg_match('#^(' . $allowedFunctions . ')\(#i', $expression, $match))
  998. {
  999. $expression = substr($expression, strlen($match[0]));
  1000. $compiled .= $match[1] . $this->_parseConditionExpression($expression, $placeholders, true, true);
  1001. $state = 'operator';
  1002. continue; // not eating anything, so must continue
  1003. }
  1004. else if (preg_match('#^(\'|")#', $expression, $match))
  1005. {
  1006. $quoteClosePos = strpos($expression, $match[0], 1); // skip initial
  1007. if ($quoteClosePos === false)
  1008. {
  1009. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
  1010. }
  1011. $quoted = substr($expression, 1, $quoteClosePos - 1);
  1012. $string = array();
  1013. $i = 0;
  1014. foreach (explode("\x1A", $quoted) AS $quotedPart)
  1015. {
  1016. if ($i % 2 == 1)
  1017. {
  1018. // odd parts have a ? before them
  1019. $string[] = array_shift($placeholders);
  1020. }
  1021. if ($quotedPart !== '')
  1022. {
  1023. $string[] = "'" . $this->escapeSingleQuotedString($quotedPart) . "'";
  1024. }
  1025. $i++;
  1026. }
  1027. if (!$string)
  1028. {
  1029. $string[] = "''";
  1030. }
  1031. $compiled .= '(' . implode(' . ', $string) . ')';
  1032. $eatChars = strlen($quoted) + 2; // 2 = quotes on either side
  1033. $state = 'operator';
  1034. }
  1035. }
  1036. else if ($state == 'operator')
  1037. {
  1038. if (preg_match('#^\s+#', $expression, $match))
  1039. {
  1040. // ignore whitespace
  1041. $eatChars = strlen($match[0]);
  1042. }
  1043. else if (preg_match('#^(\*|\+|\-|/|%|===|==|!==|!=|>=|<=|<|>|\|\||&&|and|or|xor|&|\|)#i', $expression, $match))
  1044. {
  1045. $eatChars = strlen($match[0]);
  1046. $compiled .= " $match[0] ";
  1047. $state = 'value';
  1048. }
  1049. else if ($expression[0] == ')' && $internalExpression)
  1050. {
  1051. // eat and return successfully
  1052. $eatChars = 1;
  1053. $state = false;
  1054. }
  1055. else if ($expression[0] == ',' && $isFunction)
  1056. {
  1057. $eatChars = 1;
  1058. $compiled .= ", ";
  1059. $state = 'value';
  1060. }
  1061. }
  1062. if ($eatChars)
  1063. {
  1064. $expression = substr($expression, $eatChars);
  1065. }
  1066. else
  1067. {
  1068. // prevent infinite loops -- if you want to avoid this, use "continue"
  1069. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
  1070. }
  1071. } while ($state !== false && $expression !== '' && $expression !== false);
  1072. if ($state != $endState && $state !== false)
  1073. {
  1074. // operator is the end state -- means we're expecting an operator, so it can be anything
  1075. throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
  1076. }
  1077. return "($compiled)";
  1078. }
  1079. /**
  1080. * Gets the literal value of a curly function's argument. If a literal value
  1081. * cannot be obtained, false is returned.
  1082. *
  1083. * @param string|array $argument
  1084. *
  1085. * @return string|false Literal value or false
  1086. */
  1087. public function getArgumentLiteralValue($argument)
  1088. {
  1089. if (is_string($argument))
  1090. {
  1091. return $argument;
  1092. }
  1093. else if (is_array($argument) && sizeof($argument) == 1 && is_string($argument[0]))
  1094. {
  1095. return $argument[0];
  1096. }
  1097. else
  1098. {
  1099. return false;
  1100. }
  1101. }
  1102. /**
  1103. * Quickly gets multiple named attributes and returns any of them that exist.
  1104. *
  1105. * @param array Attributes for the tag
  1106. * @param array Attributes to fetch
  1107. *
  1108. * @return array Any attributes that existed
  1109. */
  1110. public function getNamedAttributes(array $attributes, array $wantedAttributes)
  1111. {
  1112. $output = array();
  1113. foreach ($wantedAttributes AS $wanted)
  1114. {
  1115. if (isset($attributes[$wanted]))
  1116. {
  1117. $output[$wanted] = $attributes[$wanted];
  1118. }
  1119. }
  1120. return $output;
  1121. }
  1122. /**
  1123. * Sets whether external data (phrases, includes) should be "followed" and fetched.
  1124. * This can be set to false when doing a test compile.
  1125. *
  1126. * @param boolean $value
  1127. */
  1128. public function setFollowExternal($value)
  1129. {
  1130. $this->_followExternal = (bool)$value;
  1131. }
  1132. /**
  1133. * Sets the line number manually. This may be needed to report a more
  1134. * accurate line number when tags manually handle child tags (eg, if).
  1135. *
  1136. * If this function is not used, the line number from the last tag
  1137. * handled by {@link compileSegment()} will be used.
  1138. *
  1139. * @param integer $lineNumber
  1140. */
  1141. public function setLineNumber($lineNumber)
  1142. {
  1143. $this->_lineNumber = intval($lineNumber);
  1144. }
  1145. /**
  1146. * Gets the current line number.
  1147. *
  1148. * @return integer
  1149. */
  1150. public function getLineNumber()
  1151. {
  1152. return $this->_lineNumber;
  1153. }
  1154. /**
  1155. * Merges phrases into the existing phrase cache. Phrases are expected
  1156. * for all languages.
  1157. *
  1158. * @param array $phraseData Format: [language id][title] => value
  1159. */
  1160. public function mergePhraseCache(array $phraseData)
  1161. {
  1162. foreach ($phraseData AS $languageId => $phrases)
  1163. {
  1164. if (!is_array($phrases))
  1165. {
  1166. continue;
  1167. }
  1168. if (isset(self::$_phraseCache[$languageId]))
  1169. {
  1170. self::$_phraseCache[$languageId] = array_merge(self::$_phraseCache[$languageId], $phrases);
  1171. }
  1172. else
  1173. {
  1174. self::$_phraseCache[$languageId] = $phrases;
  1175. }
  1176. }
  1177. }
  1178. /**
  1179. * Resets the phrase cache. This should be done when a phrase value
  1180. * changes, before compiling templates.
  1181. */
  1182. public static function resetPhraseCache()
  1183. {
  1184. self::$_phraseCache = array();
  1185. }
  1186. /**
  1187. * Gets the value for a phrase in the language the compiler is compiling for.
  1188. *
  1189. * @param string $title
  1190. *
  1191. * @return string|false
  1192. */
  1193. public function getPhraseValue($title)
  1194. {
  1195. if (!$this->_followExternal)
  1196. {
  1197. return false;
  1198. }
  1199. $this->_includedPhrases[$title] = true;
  1200. if (isset(self::$_phraseCache[$this->_languageId][$title]))
  1201. {
  1202. return self::$_phraseCache[$this->_languageId][$title];
  1203. }
  1204. else
  1205. {
  1206. return false;
  1207. }
  1208. }
  1209. /**
  1210. * Disables dynamic phrase loading. Generally only desired for tests.
  1211. */
  1212. public function disableDynamicPhraseLoad()
  1213. {
  1214. $this->_enableDynamicPhraseLoad = false;
  1215. }
  1216. /**
  1217. * Gets a list of all phrases included in this template.
  1218. *
  1219. * @return array List of phrase titles
  1220. */
  1221. public function getIncludedPhrases()
  1222. {
  1223. return array_keys($this->_includedPhrases);
  1224. }
  1225. /**
  1226. * Gets an already parsed template for inclusion.
  1227. *
  1228. * @param string $title Name of the template to include
  1229. *
  1230. * @return string|array Segments
  1231. */
  1232. public function includeParsedTemplate($title)
  1233. {
  1234. if ($title == $this->_title)
  1235. {
  1236. throw $this->getNewCompilerException(new XenForo_Phrase('circular_reference_found_in_template_includes'));
  1237. }
  1238. if (!$this->_followExternal)
  1239. {
  1240. return '';
  1241. }
  1242. if (!isset(self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title]))
  1243. {
  1244. self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title] = $this->_getParsedTemplateFromModel($title, $this->_styleId);
  1245. }
  1246. $info = self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title];
  1247. if (is_array($info))
  1248. {
  1249. if (empty($this->_includedTemplates[$info['id']]))
  1250. {
  1251. // cache phrases for this template as we haven't included it
  1252. $this->_findAndLoadPhrasesFromSegments($info['data']);
  1253. }
  1254. $this->_includedTemplates[$info['id']] = true;
  1255. return $info['data'];
  1256. }
  1257. else
  1258. {
  1259. return '';
  1260. }
  1261. }
  1262. /**
  1263. * Finds the phrases used by the specified segments, loads them, and then
  1264. * merges them into the local phrase cache.
  1265. *
  1266. * @param array|string $segments
  1267. */
  1268. protected function _findAndLoadPhrasesFromSegments($segments)
  1269. {
  1270. if (!$this->_enableDynamicPhraseLoad)
  1271. {
  1272. return;
  1273. }
  1274. $phrasesUsed = $this->identifyPhrasesInParsedTemplate($segments);
  1275. foreach ($phrasesUsed AS $key => $title)
  1276. {
  1277. if (isset(self::$_phraseCache[$this->_languageId][$title]))
  1278. {
  1279. unset($phrasesUsed[$key]);
  1280. }
  1281. }
  1282. if ($phrasesUsed)
  1283. {
  1284. $phraseData = XenForo_Model::create('XenForo_Model_Phrase')->getEffectivePhraseValuesInAllLanguages($phrasesUsed);
  1285. $this->mergePhraseCache($phraseData);
  1286. }
  1287. }
  1288. /**
  1289. * Helper to go to the model to get the parsed version of the specified template.
  1290. *
  1291. * @param string $title Title of template
  1292. * @param integer $styleId ID of the style the template should apply to
  1293. *
  1294. * @return false|array Array should have keys of id and data (data should be parsed version of template)
  1295. */
  1296. protected function _getParsedTemplateFromModel($title, $styleId)
  1297. {
  1298. $template = XenForo_Model::create('XenForo_Model_Template')->getEffectiveTemplateByTitle($title, $styleId);
  1299. if (isset($template['template_parsed']))
  1300. {
  1301. return array(
  1302. 'id' => $template['template_map_id'],
  1303. 'data' => unserialize($template['template_parsed'])
  1304. );
  1305. }
  1306. else
  1307. {
  1308. return false;
  1309. }
  1310. }
  1311. /**
  1312. * Adds parsed templates to the template cache for the specified style.
  1313. *
  1314. * @param array $templates Keys are template names, values are parsed vesions of templates
  1315. * @param integer $styleId ID of the style that the templates are from
  1316. */
  1317. public static function setTemplateCache(array $templates, $styleId = 0)
  1318. {
  1319. self::_setTemplateCache($templates, $styleId, self::$_compilerType);
  1320. }
  1321. /**
  1322. * Internal handler for setting the template cache.
  1323. *
  1324. * @param array $templates
  1325. * @param integer $styleId
  1326. * @param string $compilerType
  1327. */
  1328. protected static function _setTemplateCache(array $templates, $styleId, $compilerType)
  1329. {
  1330. if (empty(self::$_templateCache[$compilerType][$styleId]))
  1331. {
  1332. self::$_templateCache[$compilerType][$styleId] = $templates;
  1333. }
  1334. else
  1335. {
  1336. self::$_templateCache[$compilerType][$styleId] = array_merge(self::$_templateCache[$compilerType][$styleId], $templates);
  1337. }
  1338. }
  1339. /**
  1340. * Helper to reset the template cache to reclaim memory or for tests.
  1341. *
  1342. * @param integer|true $styleId Style ID to reset the cache for; true for all styles
  1343. */
  1344. public static function resetTemplateCache($styleId = true)
  1345. {
  1346. self::_resetTemplateCache($styleId, self::$_compilerType);
  1347. }
  1348. /**
  1349. * Internal handler for resetting the template cache.
  1350. *
  1351. * @param integer|boolean $styleId
  1352. * @param string $compilerType
  1353. */
  1354. protected static function _resetTemplateCache($styleId, $compilerType)
  1355. {
  1356. if ($styleId === true)
  1357. {
  1358. self::$_templateCache[$compilerType] = array();
  1359. }
  1360. else
  1361. {
  1362. self::$_templateCache[$compilerType][$styleId] = array();
  1363. }
  1364. }
  1365. /**
  1366. * Removes the named template from the compiler cache.
  1367. *
  1368. * @param string $title
  1369. */
  1370. public static function removeTemplateFromCache($title)
  1371. {
  1372. self::_removeTemplateFromCache($title, self::$_compilerType);
  1373. }
  1374. /**
  1375. * Internal handler to remove the named template from the specified compiler
  1376. * cache.
  1377. *
  1378. * @param string $title
  1379. * @param string $compilerType
  1380. */
  1381. protected static function _removeTemplateFromCache($title, $compilerType)
  1382. {
  1383. if (!$title || !isset(self::$_templateCache[$compilerType]))
  1384. {
  1385. return;
  1386. }
  1387. foreach (self::$_templateCache[$compilerType] AS $styleId => $style)
  1388. {
  1389. if (isset($style[$title]))
  1390. {
  1391. unset(self::$_templateCache[$compilerType][$styleId][$title]);
  1392. }
  1393. }
  1394. }
  1395. /**
  1396. * Gets the list of included template IDs (map or actual template IDs).
  1397. *
  1398. * @return array
  1399. */
  1400. public function getIncludedTemplates()
  1401. {
  1402. return array_keys($this->_includedTemplates);
  1403. }
  1404. /**
  1405. * Gets the compiler type. This method generally needs to be overridden
  1406. * in child classes because of the lack of LSB.
  1407. *
  1408. * @return string
  1409. */
  1410. public function getCompilerType()
  1411. {
  1412. return self::$_compilerType;
  1413. }
  1414. /**
  1415. * Identifies the list of phrases that exist in a parsed template. This list
  1416. * can be populated even if the template is invalid.
  1417. *
  1418. * @param string|array $segments List of parsed segments
  1419. *
  1420. * @return array Unique list of phrases used in this template
  1421. */
  1422. public function identifyPhrasesInParsedTemplate($segments)
  1423. {
  1424. $phrases = $this->_identifyPhrasesInSegments($segments);
  1425. return array_unique($phrases);
  1426. }
  1427. /**
  1428. * Internal handler to get the phrases that are used in a collection of
  1429. * template segments.
  1430. *
  1431. * @param string|array $segments
  1432. *
  1433. * @return array List of phrases used in these segments; phrases may be repeated
  1434. */
  1435. protected function _identifyPhrasesInSegments($segments)
  1436. {
  1437. $phrases = array();
  1438. foreach ($this->prepareSegmentsForIteration($segments) AS $segment)
  1439. {
  1440. if (!is_array($segment) || !isset($segment['type']))
  1441. {
  1442. continue;
  1443. }
  1444. switch ($segment['type'])
  1445. {
  1446. case 'TAG':
  1447. $phrases = array_merge($phrases,
  1448. $this->_identifyPhrasesInSegments($segment['children'])
  1449. );
  1450. foreach ($segment['attributes'] AS $attribute)
  1451. {
  1452. $phrases = array_merge($phrases, $this->_identifyPhrasesInSegments($attribute));
  1453. }
  1454. break;
  1455. case 'CURLY_FUNCTION':
  1456. if ($segment['name'] == 'phrase' && isset($segment['arguments'][0]))
  1457. {
  1458. $literalValue = $this->getArgumentLiteralValue($segment['arguments'][0]);
  1459. if ($literalValue !== false)
  1460. {
  1461. $phrases[] = $literalValue;
  1462. }
  1463. }
  1464. foreach ($segment['arguments'] AS $argument)
  1465. {
  1466. $phrases = array_merge($phrases, $this->_identifyPhrasesInSegments($argument));
  1467. }
  1468. }
  1469. }
  1470. return $phrases;
  1471. }
  1472. /**
  1473. * Resolves the mapping for the specified variable.
  1474. *
  1475. * @param string $name
  1476. * @param array $options Compiler options
  1477. *
  1478. * @return string
  1479. */
  1480. public function resolveMappedVariable($name, array $options)
  1481. {
  1482. if (!empty($options['disableVarMap']))
  1483. {
  1484. return $name;
  1485. }
  1486. $visited = array(); // loop protection
  1487. while (isset($this->_variableMap[$name]) && !isset($visited[$name]))
  1488. {
  1489. $visited[$name] = true;
  1490. $name = $this->_variableMap[$name];
  1491. }
  1492. return $name;
  1493. }
  1494. /**
  1495. * Gets the variable map list.
  1496. *
  1497. * @return array
  1498. */
  1499. public function getVariableMap()
  1500. {
  1501. return $this->_variableMap;
  1502. }
  1503. /**
  1504. * Sets/merges the variable map list.
  1505. *
  1506. * @param array $map
  1507. * @param boolean $merge If true, merges; otherwise, overwrites
  1508. */
  1509. public function setVariableMap(array $map, $merge = false)
  1510. {
  1511. if ($merge)
  1512. {
  1513. if ($map)
  1514. {
  1515. $this->_variableMap = array_merge($this->_variableMap, $map);
  1516. }
  1517. }
  1518. else
  1519. {
  1520. $this->_variableMap = $map;
  1521. }
  1522. }
  1523. }