PageRenderTime 58ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/_app/vendor/Lex/Parser.php

https://bitbucket.org/sirestudios/fortis-wellness
PHP | 1400 lines | 831 code | 232 blank | 337 comment | 310 complexity | 4071bddcbf32bd88ae2178b057e588b1 MD5 | raw file
Possible License(s): JSON

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Part of the Lex Template Parser
  4. *
  5. * @author Dan Horrigan
  6. * @author Jack McDade
  7. * @author Fred LeBlanc
  8. * @author Mubashar Iqbal
  9. * @license MIT License
  10. * @copyright 2011 - 2012 Dan Horrigan
  11. * @copyright 2013 Statamic (the Statamic-specific changes)
  12. */
  13. namespace Lex;
  14. class Parser
  15. {
  16. protected $allowPhp = false;
  17. protected $regexSetup = false;
  18. protected $scopeGlue = '.';
  19. protected $tagRegex = '';
  20. protected $cumulativeNoparse = false;
  21. protected $inCondition = false;
  22. protected $variableRegex = '';
  23. protected $variableLoopRegex = '';
  24. protected $variableTagRegex = '';
  25. protected $callbackTagRegex = '';
  26. protected $callbackLoopTagRegex = '';
  27. protected $callbackNameRegex = '';
  28. protected $callbackBlockRegex = '';
  29. protected $noparseRegex = '';
  30. protected $recursiveRegex = '';
  31. protected $conditionalRegex = '';
  32. protected $conditionalElseRegex = '';
  33. protected $conditionalEndRegex = '';
  34. protected $conditionalData = array();
  35. protected $conditionalNotRegex = '';
  36. protected $conditionalExistsRegex = '';
  37. protected static $extractions = array(
  38. 'noparse' => array(),
  39. );
  40. protected static $data = null;
  41. protected static $callbackData = array();
  42. /**
  43. * The main Lex parser method. Essentially acts as dispatcher to
  44. * all of the helper parser methods.
  45. *
  46. * @param string $text Text to parse
  47. * @param array|object $data Array or object to use
  48. * @param mixed $callback Callback to use for Callback Tags
  49. * @param boolean $allowPhp Should we allow PHP?
  50. * @return string
  51. */
  52. public function parse($text, $data = array(), $callback = false, $allowPhp = false)
  53. {
  54. // <statamic>
  55. // use : as scope-glue
  56. $this->scopeGlue = ':';
  57. // </statamic>
  58. $this->setupRegex();
  59. $this->allowPhp = $allowPhp;
  60. // Is this the first time parse() is called?
  61. if (self::$data === null) {
  62. // Let's store the local data array for later use.
  63. self::$data = $data;
  64. } else {
  65. // Let's merge the current data array with the local scope variables
  66. // So you can call local variables from within blocks.
  67. $data = array_merge(self::$data, $data);
  68. // Since this is not the first time parse() is called, it's most definately a callback,
  69. // let's store the current callback data with the the local data
  70. // so we can use it straight after a callback is called.
  71. self::$callbackData = $data;
  72. }
  73. // The parseConditionals method executes any PHP in the text, so clean it up.
  74. if (! $allowPhp) {
  75. $text = str_replace(array('<?', '?>'), array('&lt;?', '?&gt;'), $text);
  76. }
  77. // <statamic>
  78. // reverse the order of no-parse and commenting
  79. $text = $this->extractNoparse($text);
  80. $text = $this->parseComments($text);
  81. // </statamic>
  82. $text = $this->extractLoopedTags($text, $data, $callback);
  83. // Order is important here. We parse conditionals first as to avoid
  84. // unnecessary code from being parsed and executed.
  85. $text = $this->parseConditionals($text, $data, $callback);
  86. $text = $this->injectExtractions($text, 'looped_tags');
  87. $text = $this->parseVariables($text, $data, $callback);
  88. $text = $this->injectExtractions($text, 'callback_blocks');
  89. if ($callback) {
  90. $text = $this->parseCallbackTags($text, $data, $callback);
  91. }
  92. // To ensure that {{ noparse }} is never parsed even during consecutive parse calls
  93. // set $cumulativeNoparse to true and use self::injectNoparse($text); immediately
  94. // before the final output is sent to the browser
  95. if (! $this->cumulativeNoparse) {
  96. $text = $this->injectExtractions($text);
  97. }
  98. // <statamic>
  99. // get tag-pairs with parameters to work
  100. if (strpos($text, "{{") !== false) {
  101. $text = $this->parseCallbackTags($text, $data, null);
  102. }
  103. // </statamic>
  104. return $text;
  105. }
  106. /**
  107. * Removes all of the comments from the text.
  108. *
  109. * @param string $text Text to remove comments from
  110. * @return string
  111. */
  112. public function parseComments($text)
  113. {
  114. $this->setupRegex();
  115. return preg_replace('/\{\{#.*?#\}\}/s', '', $text);
  116. }
  117. /**
  118. * Recursively parses all of the variables in the given text and
  119. * returns the parsed text.
  120. *
  121. * @param string $text Text to parse
  122. * @param array|object $data Array or object to use
  123. * @param callable $callback Callback function to call
  124. * @return string
  125. */
  126. public function parseVariables($text, $data, $callback = null)
  127. {
  128. $this->setupRegex();
  129. /**
  130. * $data_matches[][0][0] is the raw data loop tag
  131. * $data_matches[][0][1] is the offset of raw data loop tag
  132. * $data_matches[][1][0] is the data variable (dot notated)
  133. * $data_matches[][1][1] is the offset of data variable
  134. * $data_matches[][2][0] is the content to be looped over
  135. * $data_matches[][2][1] is the offset of content to be looped over
  136. */
  137. if (preg_match_all($this->variableLoopRegex, $text, $data_matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE)) {
  138. foreach ($data_matches as $match) {
  139. $loop_data = $this->getVariable($match[1][0], $data);
  140. if ($loop_data) {
  141. $looped_text = '';
  142. $index = 0;
  143. // <statamic>
  144. // is this data an array?
  145. if (is_array($loop_data)) {
  146. // yes
  147. $total_results = count($loop_data);
  148. foreach ($loop_data as $loop_key => $loop_value) {
  149. $index++;
  150. $new_loop = (is_array($loop_value)) ? $loop_value : array($loop_key => $loop_value);
  151. // is the value an array?
  152. if ( ! is_array($loop_value)) {
  153. // no, make it one
  154. $loop_value = array(
  155. 'value' => $loop_value,
  156. 'name' => $loop_value // 'value' alias (legacy)
  157. );
  158. }
  159. // set contextual iteration values
  160. $loop_value['key'] = $loop_key;
  161. $loop_value['index'] = $index;
  162. $loop_value['zero_index'] = $index - 1;
  163. $loop_value['total_results'] = $total_results;
  164. $loop_value['first'] = ($index === 1) ? true : false;
  165. $loop_value['last'] = ($index === $loop_value['total_results']) ? true : false;
  166. // merge this local data with callback data before performing actions
  167. $loop_value = array_merge(self::$callbackData, $loop_value);
  168. // perform standard actions
  169. $str = $this->extractLoopedTags($match[2][0], $loop_value, $callback);
  170. $str = $this->parseConditionals($str, $loop_value, $callback);
  171. $str = $this->injectExtractions($str, 'looped_tags');
  172. $str = $this->parseVariables($str, $loop_value, $callback);
  173. if (!is_null($callback)) {
  174. $str = $this->parseCallbackTags($str, $new_loop, $callback);
  175. }
  176. $looped_text .= $str;
  177. }
  178. $text = preg_replace('/'.preg_quote($match[0][0], '/').'/m', addcslashes($looped_text, '\\$'), $text, 1);
  179. } else {
  180. // no, so this is just a value, we're done here
  181. return $loop_data;
  182. }
  183. // </statamic>
  184. } else { // It's a callback block.
  185. // Let's extract it so it doesn't conflict
  186. // with the local scope variables in the next step.
  187. $text = $this->createExtraction('callback_blocks', $match[0][0], $match[0][0], $text);
  188. }
  189. }
  190. }
  191. /**
  192. * $data_matches[0] is the raw data tag
  193. * $data_matches[1] is the data variable (dot notated)
  194. */
  195. if (preg_match_all($this->variableTagRegex, $text, $data_matches)) {
  196. foreach ($data_matches[1] as $index => $var) {
  197. if (($val = $this->getVariable($var, $data, '__lex_no_value__')) !== '__lex_no_value__') {
  198. if (is_array($val)) {
  199. $val = "";
  200. \Log::error("Cannot display tag `" . $data_matches[0][$index] . "` because it is a list, not a single value. To display list values, use a tag-pair.", "template", "parser");
  201. }
  202. $text = str_replace($data_matches[0][$index], $val, $text);
  203. }
  204. }
  205. }
  206. // <statamic>
  207. // we need to look for parameters on plain-old non-callback variable tags
  208. // right now, this only applies to `format` parameters for dates
  209. $regex = '/\{\{\s*('.$this->variableRegex.')(\s+.*?)?\s*\}\}/ms';
  210. if (preg_match_all($regex, $text, $data_matches, PREG_SET_ORDER + PREG_OFFSET_CAPTURE)) {
  211. foreach ($data_matches as $match) {
  212. // grab some starting values & init variables
  213. $parameters = array();
  214. $tag = $match[0][0];
  215. $name = $match[1][0];
  216. // is this not the content tag, and is the value known?
  217. if ($name != 'content' && isset($data[$name])) {
  218. // it is, are there parameters?
  219. if (isset($match[2])) {
  220. // there are, make a backup of our $data
  221. $cb_data = $data;
  222. // is $data an array?
  223. if (is_array($data)) {
  224. // it is, have we had callback data before?
  225. if ( !empty(self::$callbackData)) {
  226. // we have, merge it all together
  227. $cb_data = array_merge(self::$callbackData, $data);
  228. }
  229. // grab the raw string of parameters
  230. $raw_params = $this->injectExtractions($match[2][0], '__cond_str');
  231. // parse them into an array
  232. $parameters = $this->parseParameters($raw_params, $cb_data, $callback);
  233. } elseif (is_string($data)) {
  234. $text = str_replace($tag, $data, $text);
  235. }
  236. }
  237. // check for certain parameters and do what they should do
  238. if (isset($parameters['format'])) {
  239. $text = str_replace($tag, \Date::format($parameters['format'], $data[$name]), $text);
  240. }
  241. }
  242. }
  243. }
  244. // </statamic>
  245. return $text;
  246. }
  247. /**
  248. * Parses all Callback tags, and sends them through the given $callback.
  249. *
  250. * @param string $text Text to parse
  251. * @param array $data An array of data to use
  252. * @param mixed $callback Callback to apply to each tag
  253. * @return string
  254. */
  255. public function parseCallbackTags($text, $data, $callback)
  256. {
  257. $this->setupRegex();
  258. $inCondition = $this->inCondition;
  259. if ($inCondition) {
  260. $regex = '/\{\s*('.$this->variableRegex.')(\s+.*?)?\s*\}/ms';
  261. } else {
  262. $regex = '/\{\{\s*('.$this->variableRegex.')(\s+.*?)?\s*(\/)?\}\}/ms';
  263. }
  264. // <statamic>
  265. // define a variable of collective callback data
  266. $cb_data = $data;
  267. // </statamic>
  268. /**
  269. * $match[0][0] is the raw tag
  270. * $match[0][1] is the offset of raw tag
  271. * $match[1][0] is the callback name
  272. * $match[1][1] is the offset of callback name
  273. * $match[2][0] is the parameters
  274. * $match[2][1] is the offset of parameters
  275. * $match[3][0] is the self closure
  276. * $match[3][1] is the offset of closure
  277. */
  278. while (preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE)) {
  279. // <statamic>
  280. // update the collective data if it's different
  281. if ( !empty(self::$callbackData)) {
  282. $cb_data = array_merge(self::$callbackData, $data);
  283. }
  284. // </statamic>
  285. $selfClosed = false;
  286. $parameters = array();
  287. $tag = $match[0][0];
  288. $start = $match[0][1];
  289. $name = $match[1][0];
  290. if (isset($match[2])) {
  291. $raw_params = $this->injectExtractions($match[2][0], '__cond_str');
  292. $parameters = $this->parseParameters($raw_params, $cb_data, $callback);
  293. // <statamic>
  294. // replace variables within parameters
  295. foreach ($parameters as $param_key => $param_value) {
  296. if (preg_match_all('/(\{\s*'.$this->variableRegex.'\s*\})/', $param_value, $param_matches)) {
  297. $param_value = str_replace('{', '{{', $param_value);
  298. $param_value = str_replace('}', '}}', $param_value);
  299. $param_value = $this->parseVariables($param_value, $data);
  300. $parameters[$param_key] = $this->parseCallbackTags($param_value, $data, $callback);
  301. }
  302. }
  303. // </statamic>
  304. }
  305. if (isset($match[3])) {
  306. $selfClosed = true;
  307. }
  308. $content = '';
  309. $temp_text = substr($text, $start + strlen($tag));
  310. if (preg_match('/\{\{\s*\/'.preg_quote($name, '/').'\s*\}\}/m', $temp_text, $match, PREG_OFFSET_CAPTURE) && ! $selfClosed) {
  311. $content = substr($temp_text, 0, $match[0][1]);
  312. $tag .= $content.$match[0][0];
  313. // Is there a nested block under this one existing with the same name?
  314. $nested_regex = '/\{\{\s*('.preg_quote($name, '/').')(\s.*?)\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
  315. if (preg_match($nested_regex, $content.$match[0][0], $nested_matches)) {
  316. $nested_content = preg_replace('/\{\{\s*\/'.preg_quote($name, '/').'\s*\}\}/m', '', $nested_matches[0]);
  317. $content = $this->createExtraction('nested_looped_tags', $nested_content, $nested_content, $content);
  318. }
  319. }
  320. // <statamic>
  321. // we'll be checking on replacement later, so initialize it
  322. $replacement = null;
  323. // now, check to see if a callback should happen
  324. if ($callback) {
  325. // </statamic>
  326. $replacement = call_user_func_array($callback, array($name, $parameters, $content));
  327. $replacement = $this->parseRecursives($replacement, $content, $callback);
  328. // <statamic>
  329. }
  330. // </statamic>
  331. // <statamic>
  332. // look for tag pairs and (plugin) callbacks
  333. if ($name != "content" && !$replacement) {
  334. // is the callback a variable in our data set?
  335. if (isset($data[$name])) {
  336. // it is, start with the value(s)
  337. $values = $data[$name];
  338. // is this a tag-pair?
  339. if (is_array($values)) {
  340. // yes it is
  341. // there might be parameters that will control how this
  342. // tag-pair's data is filtered/sorted/limited/etc,
  343. // look for those and apply those as needed
  344. // exact result grabbing ----------------------------------
  345. // first only
  346. if (isset($parameters['first'])) {
  347. $values = array_splice($values, 0, 1);
  348. }
  349. // last only
  350. if (isset($parameters['last'])) {
  351. $values = array_splice($values, -1, 1);
  352. }
  353. // specific-index only
  354. if (isset($parameters['index'])) {
  355. $values = array_splice($values, $parameters['index']-1, 1);
  356. }
  357. // now filter remaining values ----------------------------
  358. // excludes
  359. if (isset($parameters['exclude'])) {
  360. $exclude = array_flip(explode('|', $parameters['exclude']));
  361. $values = array_diff_key($values, $exclude);
  362. }
  363. // includes
  364. if (isset($parameters['include'])) {
  365. $include = array_flip(explode('|', $parameters['include']));
  366. $values = array_intersect_key($values, $include);
  367. }
  368. // now sort remaining values ------------------------------
  369. // field to sort by
  370. if (isset($parameters['sort_by'])) {
  371. $sort_field = $parameters['sort_by'];
  372. if ($sort_field == 'random') {
  373. shuffle($values);
  374. } else {
  375. usort($values, function($a, $b) use ($sort_field) {
  376. $a_value = (isset($a[$sort_field])) ? $a[$sort_field] : null;
  377. $b_value = (isset($b[$sort_field])) ? $b[$sort_field] : null;
  378. return \Helper::compareValues($a_value, $b_value);
  379. });
  380. }
  381. }
  382. // direction to sort by
  383. if (isset($parameters['sort_dir']) && $parameters['sort_dir'] == 'desc') {
  384. $values = array_reverse($values);
  385. }
  386. // finally, offset & limit values -------------------------
  387. if (isset($parameters['offset']) || isset($parameters['limit'])) {
  388. $offset = (isset($parameters['offset'])) ? $parameters['offset'] : 0;
  389. $limit = (isset($parameters['limit'])) ? $parameters['limit'] : null;
  390. $values = array_splice($values, $offset, $limit);
  391. }
  392. // loop over remaining values, adding contextual tags
  393. // to each iteration of the loop
  394. $i = 0;
  395. $total_results = count($values);
  396. foreach ($values as $value_key => $value_value) {
  397. // increment index iterator
  398. $i++;
  399. // if this isn't an array, we need to make it one
  400. if (!is_array($values[$value_key])) {
  401. // not an array, set contextual tags
  402. // note: these are for tag-pairs only
  403. $values[$value_key] = array(
  404. 'key' => $i - 1,
  405. 'value' => $values[$value_key],
  406. 'name' => $values[$value_key], // 'value' alias (legacy)
  407. 'index' => $i,
  408. 'zero_index' => $i - 1,
  409. 'total_results' => $total_results,
  410. 'first' => ($i === 1) ? true : false,
  411. 'last' => ($i === $total_results) ? true : false
  412. );
  413. }
  414. }
  415. }
  416. // is values not an empty string?
  417. if ($values !== "") {
  418. // correct, parse the tag found with the value(s) related to it
  419. $replacement = $this->parseVariables("{{ $name }}$content{{ /$name }}", array($name => $values), $callback);
  420. }
  421. } else {
  422. // nope, this must be a (plugin) callback
  423. if (is_null($callback)) {
  424. // @todo what does this do?
  425. $text = $this->createExtraction('__variables_not_callbacks', $text, $text, $text);
  426. } elseif (isset($cb_data[$name])) {
  427. // value not found in the data block, so we check the
  428. // cumulative callback data block for a value and use that
  429. $text = $this->parseVariables($text, $cb_data, $callback);
  430. $text = $this->injectExtractions($text, 'callback_blocks');
  431. }
  432. }
  433. }
  434. // </statamic>
  435. if ($inCondition) {
  436. $replacement = $this->valueToLiteral($replacement);
  437. }
  438. $text = preg_replace('/'.preg_quote($tag, '/').'/m', addcslashes($replacement, '\\$'), $text, 1);
  439. $text = $this->injectExtractions($text, 'nested_looped_tags');
  440. }
  441. // <statamic>
  442. // re-inject any extractions we extracted
  443. if (is_null($callback)) {
  444. $text = $this->injectExtractions($text, '__variables_not_callbacks');
  445. }
  446. // </statamic>
  447. return $text;
  448. }
  449. /**
  450. * Parses all conditionals, then executes the conditionals.
  451. *
  452. * @param string $text Text to parse
  453. * @param mixed $data Data to use when executing conditionals
  454. * @param mixed $callback The callback to be used for tags
  455. * @return string
  456. */
  457. public function parseConditionals($text, $data, $callback)
  458. {
  459. $this->setupRegex();
  460. preg_match_all($this->conditionalRegex, $text, $matches, PREG_SET_ORDER);
  461. $this->conditionalData = $data;
  462. /**
  463. * $matches[][0] = Full Match
  464. * $matches[][1] = Either 'if', 'unless', 'elseif', 'elseunless'
  465. * $matches[][2] = Condition
  466. */
  467. foreach ($matches as $match) {
  468. $this->inCondition = true;
  469. $condition = $match[2];
  470. // <statamic>
  471. // do an initial check for callbacks, extract them if found
  472. if ($callback) {
  473. if (preg_match_all('/\b(?!\{\s*)('.$this->callbackNameRegex.')(?!\s+.*?\s*\})\b/', $condition, $cb_matches)) {
  474. foreach ($cb_matches[0] as $m) {
  475. $condition = $this->createExtraction('__cond_callbacks', $m, "{$m}", $condition);
  476. }
  477. }
  478. }
  479. // </statamic>
  480. // Extract all literal string in the conditional to make it easier
  481. if (preg_match_all('/(["\']).*?(?<!\\\\)\1/', $condition, $str_matches)) {
  482. foreach ($str_matches[0] as $m) {
  483. $condition = $this->createExtraction('__cond_str', $m, $m, $condition);
  484. }
  485. }
  486. $condition = preg_replace($this->conditionalNotRegex, '$1!$2', $condition);
  487. if (preg_match_all($this->conditionalExistsRegex, $condition, $existsMatches, PREG_SET_ORDER)) {
  488. foreach ($existsMatches as $m) {
  489. $exists = 'true';
  490. if ($this->getVariable($m[2], $data, '__doesnt_exist__') === '__doesnt_exist__') {
  491. $exists = 'false';
  492. }
  493. $condition = $this->createExtraction('__cond_exists', $m[0], $m[1].$exists.$m[3], $condition);
  494. }
  495. }
  496. $condition = preg_replace_callback('/\b('.$this->variableRegex.')\b/', array($this, 'processConditionVar'), $condition);
  497. // <statamic>
  498. // inject any found callbacks and parse them
  499. if ($callback) {
  500. $condition = $this->injectExtractions($condition, '__cond_callbacks');
  501. $condition = $this->parseCallbackTags($condition, $data, $callback);
  502. }
  503. // </statamic>
  504. // Re-extract the strings that have now been possibly added.
  505. if (preg_match_all('/(["\']).*?(?<!\\\\)\1/', $condition, $str_matches)) {
  506. foreach ($str_matches[0] as $m) {
  507. $condition = $this->createExtraction('__cond_str', $m, $m, $condition);
  508. }
  509. }
  510. // Re-process for variables, we trick processConditionVar so that it will return null
  511. $this->inCondition = false;
  512. // <statamic>
  513. // replacements -- the preg_replace_callback below is using word boundaries, which
  514. // will break when one of your original variables gets replaced with a URL path
  515. // (because word boundaries think slashes are boundaries) -- to fix this, we replace
  516. // all instances of a literal string in single quotes with a temporary replacement
  517. $replacements = array();
  518. // first up, replacing literal strings
  519. while (preg_match("/('[^']+'|\"[^\"]+\")/", $condition, $replacement_matches)) {
  520. $replacement_match = $replacement_matches[1];
  521. $replacement_hash = md5($replacement_match);
  522. $replacements[$replacement_hash] = $replacement_match;
  523. $condition = str_replace($replacement_match, "__temp_replacement_" . $replacement_hash, $condition);
  524. }
  525. // next, the original re-processing callback
  526. // </statamic>
  527. $condition = preg_replace_callback('/\b('.$this->variableRegex.')\b/', array($this, 'processConditionVar'), $condition);
  528. // <statamic>
  529. // finally, replacing our placeholders with the original values
  530. foreach ($replacements as $replace_key => $replace_value) {
  531. $condition = str_replace('__temp_replacement_' . $replace_key, $replace_value, $condition);
  532. }
  533. // </statamic>
  534. $this->inCondition = true;
  535. // Re-inject any strings we extracted
  536. $condition = $this->injectExtractions($condition, '__cond_str');
  537. $condition = $this->injectExtractions($condition, '__cond_exists');
  538. $conditional = '<?php ';
  539. if ($match[1] == 'unless') {
  540. $conditional .= 'if ( ! ('.$condition.'))';
  541. } elseif ($match[1] == 'elseunless') {
  542. $conditional .= 'elseif ( ! ('.$condition.'))';
  543. } else {
  544. $conditional .= $match[1].' ('.$condition.')';
  545. }
  546. $conditional .= ': ?>';
  547. $text = preg_replace('/'.preg_quote($match[0], '/').'/m', addcslashes($conditional, '\\$'), $text, 1);
  548. }
  549. $text = preg_replace($this->conditionalElseRegex, '<?php else: ?>', $text);
  550. $text = preg_replace($this->conditionalEndRegex, '<?php endif; ?>', $text);
  551. $text = $this->parsePhp($text);
  552. $this->inCondition = false;
  553. return $text;
  554. }
  555. /**
  556. * Goes recursively through a callback tag with a passed child array.
  557. *
  558. * @param string $text - The replaced text after a callback.
  559. * @param string $orig_text - The original text, before a callback is called.
  560. * @param mixed $callback
  561. * @return string $text
  562. */
  563. public function parseRecursives($text, $orig_text, $callback)
  564. {
  565. // Is there a {{ *recursive [array_key]* }} tag here, let's loop through it.
  566. if (preg_match($this->recursiveRegex, $text, $match)) {
  567. $array_key = $match[1];
  568. $tag = $match[0];
  569. $next_tag = null;
  570. $children = self::$callbackData[$array_key];
  571. $child_count = count($children);
  572. $count = 1;
  573. // Is the array not multi-dimensional? Let's make it multi-dimensional.
  574. if ($child_count == count($children, COUNT_RECURSIVE)) {
  575. $children = array($children);
  576. $child_count = 1;
  577. }
  578. foreach ($children as $child) {
  579. $has_children = true;
  580. // If this is a object let's convert it to an array.
  581. is_array($child) OR $child = (array) $child;
  582. // Does this child not contain any children?
  583. // Let's set it as empty then to avoid any errors.
  584. if ( ! array_key_exists($array_key, $child)) {
  585. $child[$array_key] = array();
  586. $has_children = false;
  587. }
  588. $replacement = $this->parse($orig_text, $child, $callback, $this->allowPhp);
  589. // If this is the first loop we'll use $tag as reference, if not
  590. // we'll use the previous tag ($next_tag)
  591. $current_tag = ($next_tag !== null) ? $next_tag : $tag;
  592. // If this is the last loop set the next tag to be empty
  593. // otherwise hash it.
  594. $next_tag = ($count == $child_count) ? '' : md5($tag.$replacement);
  595. $text = str_replace($current_tag, $replacement.$next_tag, $text);
  596. if ($has_children) {
  597. $text = $this->parseRecursives($text, $orig_text, $callback);
  598. }
  599. $count++;
  600. }
  601. }
  602. return $text;
  603. }
  604. /**
  605. * Gets or sets the Scope Glue
  606. *
  607. * @param string|null $glue The Scope Glue
  608. * @return string
  609. */
  610. public function scopeGlue($glue = null)
  611. {
  612. if ($glue !== null) {
  613. $this->regexSetup = false;
  614. $this->scopeGlue = $glue;
  615. }
  616. return $this->scopeGlue;
  617. }
  618. /**
  619. * Sets the noparse style. Immediate or cumulative.
  620. *
  621. * @param bool $mode
  622. * @return void
  623. */
  624. public function cumulativeNoparse($mode)
  625. {
  626. $this->cumulativeNoparse = $mode;
  627. }
  628. /**
  629. * Injects noparse extractions.
  630. *
  631. * This is so that multiple parses can store noparse
  632. * extractions and all noparse can then be injected right
  633. * before data is displayed.
  634. *
  635. * @param string $text Text to inject into
  636. * @return string
  637. */
  638. public static function injectNoparse($text)
  639. {
  640. if (isset(self::$extractions['noparse'])) {
  641. foreach (self::$extractions['noparse'] AS $hash => $replacement) {
  642. if (strpos($text, "noparse_{$hash}") !== FALSE) {
  643. $text = str_replace("noparse_{$hash}", $replacement, $text);
  644. }
  645. }
  646. }
  647. return $text;
  648. }
  649. /**
  650. * This is used as a callback for the conditional parser. It takes a variable
  651. * and returns the value of it, properly formatted.
  652. *
  653. * @param array $match A match from preg_replace_callback
  654. * @return string
  655. */
  656. protected function processConditionVar($match)
  657. {
  658. $var = is_array($match) ? $match[0] : $match;
  659. if (in_array(strtolower($var), array('true', 'false', 'null', 'or', 'and')) or
  660. strpos($var, '__cond_str') === 0 or
  661. strpos($var, '__cond_exists') === 0 or
  662. // <statamic>
  663. // adds a new temporary replacement to deal with string literals
  664. strpos($var, '__temp_replacement') === 0 or
  665. // </statamic>
  666. is_numeric($var))
  667. {
  668. return $var;
  669. }
  670. $value = $this->getVariable($var, $this->conditionalData, '__processConditionVar__');
  671. if ($value === '__processConditionVar__') {
  672. return $this->inCondition ? $var : 'null';
  673. }
  674. return $this->valueToLiteral($value);
  675. }
  676. /**
  677. * This is used as a callback for the conditional parser. It takes a variable
  678. * and returns the value of it, properly formatted.
  679. *
  680. * @param array $match A match from preg_replace_callback
  681. * @return string
  682. */
  683. protected function processParamVar($match)
  684. {
  685. return $match[1].$this->processConditionVar($match[2]);
  686. }
  687. /**
  688. * Takes a value and returns the literal value for it for use in a tag.
  689. *
  690. * @param string $value Value to convert
  691. * @return string
  692. */
  693. protected function valueToLiteral($value)
  694. {
  695. if (is_object($value) and is_callable(array($value, '__toString'))) {
  696. return var_export((string) $value, true);
  697. } elseif (is_array($value)) {
  698. return !empty($value) ? "true" : "false";
  699. } else {
  700. return var_export($value, true);
  701. }
  702. }
  703. /**
  704. * Sets up all the global regex to use the correct Scope Glue.
  705. *
  706. * @return void
  707. */
  708. protected function setupRegex()
  709. {
  710. if ($this->regexSetup) {
  711. return;
  712. }
  713. $glue = preg_quote($this->scopeGlue, '/');
  714. // <statamic>
  715. // expand allowed characters in variable regex
  716. $this->variableRegex = $glue === '\\.' ? '[a-zA-Z0-9_][|a-zA-Z\-\+\*%\^\/,0-9_'.$glue.']*' : '[a-zA-Z0-9_][|a-zA-Z\-\+\*%\^\/,0-9_\.'.$glue.']*';
  717. // </statamic>
  718. $this->callbackNameRegex = $this->variableRegex.$glue.$this->variableRegex;
  719. $this->variableLoopRegex = '/\{\{\s*('.$this->variableRegex.')\s*\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
  720. $this->variableTagRegex = '/\{\{\s*('.$this->variableRegex.')\s*\}\}/m';
  721. // <statamic>
  722. // make the space-anything after the variable regex optional, this allows
  723. // users to use {{tags}} in addition to {{ tags }} -- weird, I know
  724. $this->callbackBlockRegex = '/\{\{\s*('.$this->variableRegex.')(\s.*?)?\}\}(.*?)\{\{\s*\/\1\s*\}\}/ms';
  725. // </statamic>
  726. $this->recursiveRegex = '/\{\{\s*\*recursive\s*('.$this->variableRegex.')\*\s*\}\}/ms';
  727. $this->noparseRegex = '/\{\{\s*noparse\s*\}\}(.*?)\{\{\s*\/noparse\s*\}\}/ms';
  728. $this->conditionalRegex = '/\{\{\s*(if|unless|elseif|elseunless)\s*((?:\()?(.*?)(?:\))?)\s*\}\}/ms';
  729. $this->conditionalElseRegex = '/\{\{\s*else\s*\}\}/ms';
  730. $this->conditionalEndRegex = '/\{\{\s*endif\s*\}\}/ms';
  731. $this->conditionalExistsRegex = '/(\s+|^)exists\s+('.$this->variableRegex.')(\s+|$)/ms';
  732. $this->conditionalNotRegex = '/(\s+|^)not(\s+|$)/ms';
  733. $this->regexSetup = true;
  734. // This is important, it's pretty unclear by the documentation
  735. // what the default value is on <= 5.3.6
  736. ini_set('pcre.backtrack_limit', 1000000);
  737. }
  738. /**
  739. * Extracts the noparse text so that it is not parsed.
  740. *
  741. * @param string $text The text to extract from
  742. * @return string
  743. */
  744. protected function extractNoparse($text)
  745. {
  746. /**
  747. * $matches[][0] is the raw noparse match
  748. * $matches[][1] is the noparse contents
  749. */
  750. if (preg_match_all($this->noparseRegex, $text, $matches, PREG_SET_ORDER)) {
  751. foreach ($matches as $match) {
  752. $text = $this->createExtraction('noparse', $match[0], $match[1], $text);
  753. }
  754. }
  755. return $text;
  756. }
  757. /**
  758. * Extracts the looped tags so that we can parse conditionals then re-inject.
  759. *
  760. * @param string $text The text to extract from
  761. * @param array $data Data array to use
  762. * @param callable $callback Callback to call when complete
  763. * @return string
  764. */
  765. protected function extractLoopedTags($text, $data = array(), $callback = null)
  766. {
  767. /**
  768. * $matches[][0] is the raw match
  769. */
  770. if (preg_match_all($this->callbackBlockRegex, $text, $matches, PREG_SET_ORDER)) {
  771. foreach ($matches as $match) {
  772. // Does this callback block contain parameters?
  773. if ($this->parseParameters($match[2], $data, $callback)) {
  774. // Let's extract it so it doesn't conflict with local variables when
  775. // parseVariables() is called.
  776. $text = $this->createExtraction('callback_blocks', $match[0], $match[0], $text);
  777. } else {
  778. $text = $this->createExtraction('looped_tags', $match[0], $match[0], $text);
  779. }
  780. }
  781. }
  782. return $text;
  783. }
  784. /**
  785. * Extracts text out of the given text and replaces it with a hash which
  786. * can be used to inject the extractions replacement later.
  787. *
  788. * @param string $type Type of extraction
  789. * @param string $extraction The text to extract
  790. * @param string $replacement Text that will replace the extraction when re-injected
  791. * @param string $text Text to extract out of
  792. * @return string
  793. */
  794. protected function createExtraction($type, $extraction, $replacement, $text)
  795. {
  796. $hash = md5($replacement);
  797. self::$extractions[$type][$hash] = $replacement;
  798. return str_replace($extraction, "{$type}_{$hash}", $text);
  799. }
  800. /**
  801. * Injects all of the extractions.
  802. *
  803. * @param string $text Text to inject into
  804. * @param string $type Type of extraction to inject
  805. * @return string
  806. */
  807. protected function injectExtractions($text, $type = null)
  808. {
  809. // <statamic>
  810. // changed === comparison to is_null check
  811. if (is_null($type)) {
  812. // </statamic>
  813. foreach (self::$extractions as $type => $extractions) {
  814. foreach ($extractions as $hash => $replacement) {
  815. if (strpos($text, "{$type}_{$hash}") !== false) {
  816. $text = str_replace("{$type}_{$hash}", $replacement, $text);
  817. unset(self::$extractions[$type][$hash]);
  818. }
  819. }
  820. }
  821. } else {
  822. if ( ! isset(self::$extractions[$type])) {
  823. return $text;
  824. }
  825. foreach (self::$extractions[$type] as $hash => $replacement) {
  826. if (strpos($text, "{$type}_{$hash}") !== false) {
  827. $text = str_replace("{$type}_{$hash}", $replacement, $text);
  828. unset(self::$extractions[$type][$hash]);
  829. }
  830. }
  831. }
  832. return $text;
  833. }
  834. /**
  835. * Takes a scope-notated key and finds the value for it in the given
  836. * array or object.
  837. *
  838. * @param string $key Dot-notated key to find
  839. * @param array|object $data Array or object to search
  840. * @param mixed $default Default value to use if not found
  841. * @return mixed
  842. */
  843. protected function getVariable($key, $data, $default = null)
  844. {
  845. $modifiers = null;
  846. if (strpos($key, "|") === false) {
  847. } else {
  848. $parts = explode("|", $key);
  849. $key = $parts[0];
  850. $modifiers = array_splice($parts, 1);
  851. }
  852. if (strpos($key, $this->scopeGlue) === false) {
  853. $parts = explode('.', $key);
  854. } else {
  855. $parts = explode($this->scopeGlue, $key);
  856. }
  857. foreach ($parts as $key_part) {
  858. if (is_array($data)) {
  859. if ( ! array_key_exists($key_part, $data)) {
  860. return $default;
  861. }
  862. $data = $data[$key_part];
  863. } elseif (is_object($data)) {
  864. if ( ! isset($data->{$key_part})) {
  865. return $default;
  866. }
  867. $data = $data->{$key_part};
  868. } else {
  869. return $default;
  870. }
  871. }
  872. if ($modifiers) {
  873. foreach ($modifiers as $mod) {
  874. if (strpos($mod, ":") === false) {
  875. $modifier_name = $mod;
  876. $modifier_params = array();
  877. } else {
  878. $parts = explode(":", $mod);
  879. $modifier_name = $parts[0];
  880. $modifier_params = array_splice($parts, 1);
  881. }
  882. // Array modifiers
  883. if ($modifier_name == 'list') {
  884. $data = join(", ", $data);
  885. } elseif ($modifier_name == 'spaced_list') {
  886. $data = join(" ", $data);
  887. } elseif ($modifier_name == 'option_list') {
  888. $data = join("|", $data);
  889. } elseif ($modifier_name == 'unordered_list') {
  890. $data = "<ol><li>" . join("</li><li>", $data) . "</li></ol>";
  891. } elseif ($modifier_name == 'ordered_list') {
  892. $data = "<ul><li>" . join("</li><li>", $data) . "</li></ul>";
  893. } elseif ($modifier_name == 'sentence_list') {
  894. $data = \Helper::makeSentenceList($data);
  895. } elseif ($modifier_name == 'ampersand_list') {
  896. $data = \Helper::makeSentenceList($data, "&", false);
  897. } elseif ($modifier_name == 'sanitize') {
  898. $data = htmlentities($data);
  899. } elseif ($modifier_name == 'json') {
  900. $data = json_encode($data);
  901. } elseif ($modifier_name == 'trim') {
  902. $data = trim($data);
  903. } elseif ($modifier_name == 'img') {
  904. $data = '<img src="' . \Path::toAsset($data) . '" />';
  905. } elseif ($modifier_name == 'link') {
  906. if (filter_var($data, FILTER_VALIDATE_EMAIL)) {
  907. // email address
  908. $data = '<a href="mailto:'.$data.'" />'.$data.'</a>';
  909. } else {
  910. $data = '<a href="'.$data.'" />'.$data.'</a>';
  911. }
  912. } elseif ($modifier_name == 'upper') {
  913. $data = strtoupper($data);
  914. } else if ($modifier_name == 'lower') {
  915. $data = strtolower($data);
  916. } else if ($modifier_name == 'slugify') {
  917. $delimiter = array_get($modifier_params, 0, '-');
  918. $data = \Slug::make($data, array('delimiter' => $delimiter));
  919. } else if ($modifier_name == 'deslugify') {
  920. $data = trim(preg_replace('~[-_]~', ' ', $data), " ");
  921. } else if ($modifier_name == 'title') {
  922. $data = ucwords($data);
  923. } else if ($modifier_name == 'format') {
  924. $data = date($modifier_params[0], $data);
  925. } else if ($modifier_name == 'format_number') {
  926. $decimals = (isset($modifier_params[0])) ? $modifier_params[0] : 0;
  927. $data = number_format($data, $decimals);
  928. } else if ($modifier_name == 'in_future') {
  929. $data = (\Date::resolve($data) > time()) ? "true" : "";
  930. } else if ($modifier_name == 'in_past') {
  931. $data = (\Date::resolve($data) < time()) ? "true" : "";
  932. } else if ($modifier_name == 'markdown') {
  933. $data = Markdown($data);
  934. } else if ($modifier_name == 'textile') {
  935. $textile = new \Textile();
  936. $data = $textile->TextileThis($data);
  937. } else if ($modifier_name == 'length') {
  938. if (is_array($data)) {
  939. $data = count($data);
  940. } else {
  941. $data = strlen($data);
  942. }
  943. } else if ($modifier_name == 'scramble') {
  944. $data = str_shuffle($data);
  945. } else if ($modifier_name == 'word_count') {
  946. $data = str_word_count($data);
  947. } else if ($modifier_name == 'obfuscate') {
  948. $data = \HTML::obfuscateEmail($data);
  949. } else if ($modifier_name == 'rot13') {
  950. $data = str_rot13($data);
  951. } else if ($modifier_name == 'urlencode') {
  952. $data = urlencode($data);
  953. } else if ($modifier_name == 'urldecode') {
  954. $data = urldecode($data);
  955. } else if ($modifier_name == 'striptags') {
  956. $data = strip_tags($data);
  957. } else if ($modifier_name == '%') {
  958. $divisor = (isset($modifier_params[0])) ? $modifier_params[0] : 1;
  959. $data = $data % $divisor;
  960. } else if ($modifier_name == 'empty') {
  961. $data = (\Helper::isEmptyArray($data)) ? "true" : "";
  962. } else if ($modifier_name == 'not_empty') {
  963. $data = (!\Helper::isEmptyArray($data)) ? "true" : "";
  964. } else if ($modifier_name == 'numeric') {
  965. $data = (is_numeric($data)) ? "true" : "";
  966. } else if ($modifier_name == 'repeat') {
  967. $multiplier = (isset($modifier_params[0])) ? $modifier_params[0] : 1;
  968. $data = str_repeat($data, $multiplier);
  969. } else if ($modifier_name == 'reverse') {
  970. $data = strrev($data);
  971. } else if ($modifier_name == 'round') {
  972. $precision = (isset($modifier_params[0])) ? (int) $modifier_params[0] : 0;
  973. $data = round((float) $data, $precision);
  974. } else if ($modifier_name == 'floor') {
  975. $data = floor((float) $data);
  976. } else if ($modifier_name == 'ceil') {
  977. $data = ceil((float) $data);
  978. } else if ($modifier_name == '+') {
  979. if (isset($modifier_params[0])) {
  980. $number = $modifier_params[0];
  981. $data = $data + $number;
  982. }
  983. } else if ($modifier_name == '-') {
  984. if (isset($modifier_params[0])) {
  985. $number = $modifier_params[0];
  986. $data = $data - $number;
  987. }
  988. } else if ($modifier_name == '*') {
  989. if (isset($modifier_params[0])) {
  990. $number = $modifier_params[0];
  991. $data = $data * $number;
  992. }
  993. } else if ($modifier_name == '/') {
  994. if (isset($modifier_params[0])) {
  995. $number = $modifier_params[0];
  996. $data = $data / $number;
  997. }
  998. } else if ($modifier_name == '^') {
  999. if (isset($modifier_params[0])) {
  1000. $exp = $modifier_params[0];
  1001. $data = pow($data, $exp);
  1002. }
  1003. } else if ($modifier_name == 'sqrt') {
  1004. $data = sqrt($data);
  1005. } else if ($modifier_name == 'abs') {
  1006. $data = abs($data);
  1007. } else if ($modifier_name == 'log') {
  1008. $base = (isset($modifier_params[0])) ? (int) $modifier_params[0] : M_E;
  1009. $data = log($data, $base);
  1010. } else if ($modifier_name == 'log10') {
  1011. $data = log10($data);
  1012. } else if ($modifier_name == 'deg2rad') {
  1013. $data = deg2rad($data);
  1014. } else if ($modifier_name == 'rad2deg') {
  1015. $data = rad2deg($data);
  1016. } else if ($modifier_name == 'sin') {
  1017. $data = sin($data);
  1018. } else if ($modifier_name == 'asin') {
  1019. $data = asin($data);
  1020. } else if ($modifier_name == 'cos') {
  1021. $data = cos($data);
  1022. } else if ($modifier_name == 'acos') {
  1023. $data = acos($data);
  1024. } else if ($modifier_name == 'tan') {
  1025. $data = tan($data);
  1026. } else if ($modifier_name == 'atan') {
  1027. $data = atan($data);
  1028. } else if ($modifier_name == 'decbin') {
  1029. $data = decbin($data);
  1030. } else if ($modifier_name == 'dechex') {
  1031. $data = dechex($data);
  1032. } else if ($modifier_name == 'decoct') {
  1033. $data = decoct($data);
  1034. } else if ($modifier_name == 'hexdec') {
  1035. $data = hexdec($data);
  1036. } else if ($modifier_name == 'octdec') {
  1037. $data = octdec($data);
  1038. } else if ($modifier_name == 'bindec') {
  1039. $data = bindec((string) $data);
  1040. } else if ($modifier_name == 'distance_in_mi_from') {
  1041. if (!isset($modifier_params[0])) {
  1042. return 'Unknown';
  1043. }
  1044. if (!preg_match(\Pattern::COORDINATES, $data, $point_1_matches)) {
  1045. return 'Unknown';

Large files files are truncated, but you can click here to view the full file