PageRenderTime 53ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/view/SSTemplateParser.php.inc

http://github.com/silverstripe/sapphire
PHP | 1236 lines | 565 code | 133 blank | 538 comment | 79 complexity | 7d35fa8e159a612003d32a34f49bf508 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. <?php
  2. /*!* !insert_autogen_warning */
  3. /*!* !silent
  4. This is the uncompiled parser for the SilverStripe template language, PHP with special comments that define the
  5. parser.
  6. It gets run through the php-peg parser compiler to have those comments turned into code that match parts of the
  7. template language, producing the executable version SSTemplateParser.php
  8. To recompile after changing this file, run this from the 'framework/view' directory via command line (in most cases
  9. this is: sapphire/view):
  10. php ../thirdparty/php-peg/cli.php SSTemplateParser.php.inc > SSTemplateParser.php
  11. See the php-peg docs for more information on the parser format, and how to convert this file into SSTemplateParser.php
  12. TODO:
  13. Template comments - <%-- --%>
  14. $Iteration
  15. Partial cache blocks
  16. i18n - we dont support then deprecated _t() or sprintf(_t()) methods; or the new <% t %> block yet
  17. Add with and loop blocks
  18. Add Up and Top
  19. More error detection?
  20. This comment will not appear in the output
  21. */
  22. // We want this to work when run by hand too
  23. if (defined(THIRDPARTY_PATH)) {
  24. require_once(THIRDPARTY_PATH . '/php-peg/Parser.php');
  25. }
  26. else {
  27. $base = dirname(__FILE__);
  28. require_once($base.'/../thirdparty/php-peg/Parser.php');
  29. }
  30. /**
  31. * This is the exception raised when failing to parse a template. Note that we don't currently do any static analysis,
  32. * so we can't know if the template will run, just if it's malformed. It also won't catch mistakes that still look
  33. * valid.
  34. *
  35. * @package framework
  36. * @subpackage view
  37. */
  38. class SSTemplateParseException extends Exception {
  39. function __construct($message, $parser) {
  40. $prior = substr($parser->string, 0, $parser->pos);
  41. preg_match_all('/\r\n|\r|\n/', $prior, $matches);
  42. $line = count($matches[0])+1;
  43. parent::__construct("Parse error in template on line $line. Error was: $message");
  44. }
  45. }
  46. /**
  47. * This is the parser for the SilverStripe template language. It gets called on a string and uses a php-peg parser
  48. * to match that string against the language structure, building up the PHP code to execute that structure as it
  49. * parses
  50. *
  51. * The $result array that is built up as part of the parsing (see thirdparty/php-peg/README.md for more on how
  52. * parsers build results) has one special member, 'php', which contains the php equivalent of that part of the
  53. * template tree.
  54. *
  55. * Some match rules generate alternate php, or other variations, so check the per-match documentation too.
  56. *
  57. * Terms used:
  58. *
  59. * Marked: A string or lookup in the template that has been explictly marked as such - lookups by prepending with
  60. * "$" (like $Foo.Bar), strings by wrapping with single or double quotes ('Foo' or "Foo")
  61. *
  62. * Bare: The opposite of marked. An argument that has to has it's type inferred by usage and 2.4 defaults.
  63. *
  64. * Example of using a bare argument for a loop block: <% loop Foo %>
  65. *
  66. * Block: One of two SS template structures. The special characters "<%" and "%>" are used to wrap the opening and
  67. * (required or forbidden depending on which block exactly) closing block marks.
  68. *
  69. * Open Block: An SS template block that doesn't wrap any content or have a closing end tag (in fact, a closing end
  70. * tag is forbidden)
  71. *
  72. * Closed Block: An SS template block that wraps content, and requires a counterpart <% end_blockname %> tag
  73. *
  74. * Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
  75. * N: eats white space including newlines (using in legacy _t support)
  76. *
  77. * @package framework
  78. * @subpackage view
  79. */
  80. class SSTemplateParser extends Parser implements TemplateParser {
  81. /**
  82. * @var bool - Set true by SSTemplateParser::compileString if the template should include comments intended
  83. * for debugging (template source, included files, etc)
  84. */
  85. protected $includeDebuggingComments = false;
  86. /**
  87. * Stores the user-supplied closed block extension rules in the form:
  88. * array(
  89. * 'name' => function (&$res) {}
  90. * )
  91. * See SSTemplateParser::ClosedBlock_Handle_Loop for an example of what the callable should look like
  92. * @var array
  93. */
  94. protected $closedBlocks = array();
  95. /**
  96. * Stores the user-supplied open block extension rules in the form:
  97. * array(
  98. * 'name' => function (&$res) {}
  99. * )
  100. * See SSTemplateParser::OpenBlock_Handle_Base_tag for an example of what the callable should look like
  101. * @var array
  102. */
  103. protected $openBlocks = array();
  104. /**
  105. * Allow the injection of new closed & open block callables
  106. * @param array $closedBlocks
  107. * @param array $openBlocks
  108. */
  109. public function __construct($closedBlocks = array(), $openBlocks = array()) {
  110. $this->setClosedBlocks($closedBlocks);
  111. $this->setOpenBlocks($openBlocks);
  112. }
  113. /**
  114. * Override the function that constructs the result arrays to also prepare a 'php' item in the array
  115. */
  116. function construct($matchrule, $name, $arguments = null) {
  117. $res = parent::construct($matchrule, $name, $arguments);
  118. if (!isset($res['php'])) $res['php'] = '';
  119. return $res;
  120. }
  121. /**
  122. * Set the closed blocks that the template parser should use
  123. *
  124. * This method will delete any existing closed blocks, please use addClosedBlock if you don't
  125. * want to overwrite
  126. * @param array $closedBlocks
  127. * @throws InvalidArgumentException
  128. */
  129. public function setClosedBlocks($closedBlocks) {
  130. $this->closedBlocks = array();
  131. foreach ((array) $closedBlocks as $name => $callable) {
  132. $this->addClosedBlock($name, $callable);
  133. }
  134. }
  135. /**
  136. * Set the open blocks that the template parser should use
  137. *
  138. * This method will delete any existing open blocks, please use addOpenBlock if you don't
  139. * want to overwrite
  140. * @param array $openBlocks
  141. * @throws InvalidArgumentException
  142. */
  143. public function setOpenBlocks($openBlocks) {
  144. $this->openBlocks = array();
  145. foreach ((array) $openBlocks as $name => $callable) {
  146. $this->addOpenBlock($name, $callable);
  147. }
  148. }
  149. /**
  150. * Add a closed block callable to allow <% name %><% end_name %> syntax
  151. * @param string $name The name of the token to be used in the syntax <% name %><% end_name %>
  152. * @param callable $callable The function that modifies the generation of template code
  153. * @throws InvalidArgumentException
  154. */
  155. public function addClosedBlock($name, $callable) {
  156. $this->validateExtensionBlock($name, $callable, 'Closed block');
  157. $this->closedBlocks[$name] = $callable;
  158. }
  159. /**
  160. * Add a closed block callable to allow <% name %> syntax
  161. * @param string $name The name of the token to be used in the syntax <% name %>
  162. * @param callable $callable The function that modifies the generation of template code
  163. * @throws InvalidArgumentException
  164. */
  165. public function addOpenBlock($name, $callable) {
  166. $this->validateExtensionBlock($name, $callable, 'Open block');
  167. $this->openBlocks[$name] = $callable;
  168. }
  169. /**
  170. * Ensures that the arguments to addOpenBlock and addClosedBlock are valid
  171. * @param $name
  172. * @param $callable
  173. * @param $type
  174. * @throws InvalidArgumentException
  175. */
  176. protected function validateExtensionBlock($name, $callable, $type) {
  177. if (!is_string($name)) {
  178. throw new InvalidArgumentException(
  179. sprintf(
  180. "Name argument for %s must be a string",
  181. $type
  182. )
  183. );
  184. } elseif (!is_callable($callable)) {
  185. throw new InvalidArgumentException(
  186. sprintf(
  187. "Callable %s argument named '%s' is not callable",
  188. $type,
  189. $name
  190. )
  191. );
  192. }
  193. }
  194. /*!* SSTemplateParser
  195. # Template is any structurally-complete portion of template (a full nested level in other words). It's the
  196. # primary matcher, and is used by all enclosing blocks, as well as a base for the top level.
  197. # Any new template elements need to be included in this list, if they are to work.
  198. Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock |
  199. OpenBlock | MalformedBlock | Injection | Text)+
  200. */
  201. function Template_STR(&$res, $sub) {
  202. $res['php'] .= $sub['php'] . PHP_EOL ;
  203. }
  204. /*!*
  205. Word: / [A-Za-z_] [A-Za-z0-9_]* /
  206. NamespacedWord: / [A-Za-z_\/\\] [A-Za-z0-9_\/\\]* /
  207. Number: / [0-9]+ /
  208. Value: / [A-Za-z0-9_]+ /
  209. # CallArguments is a list of one or more comma seperated "arguments" (lookups or strings, either bare or marked)
  210. # as passed to a Call within brackets
  211. CallArguments: :Argument ( < "," < :Argument )*
  212. */
  213. /**
  214. * Values are bare words in templates, but strings in PHP. We rely on PHP's type conversion to back-convert
  215. * strings to numbers when needed.
  216. */
  217. function CallArguments_Argument(&$res, $sub) {
  218. if (!empty($res['php'])) $res['php'] .= ', ';
  219. $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] :
  220. str_replace('$$FINAL', 'XML_val', $sub['php']);
  221. }
  222. /*!*
  223. # Call is a php-style function call, e.g. Method(Argument, ...). Unlike PHP, the brackets are optional if no
  224. # arguments are passed
  225. Call: Method:Word ( "(" < :CallArguments? > ")" )?
  226. # A lookup is a lookup of a value on the current scope object. It's a sequence of calls seperated by "."
  227. # characters. This final call in the sequence needs handling specially, as different structures need different
  228. # sorts of values, which require a different final method to be called to get the right return value
  229. LookupStep: :Call &"."
  230. LastLookupStep: :Call
  231. Lookup: LookupStep ("." LookupStep)* "." LastLookupStep | LastLookupStep
  232. */
  233. function Lookup__construct(&$res) {
  234. $res['php'] = '$scope->locally()';
  235. $res['LookupSteps'] = array();
  236. }
  237. /**
  238. * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to
  239. * get the next ViewableData in the sequence, and LastLookupStep calls different methods (XML_val, hasValue, obj)
  240. * depending on the context the lookup is used in.
  241. */
  242. function Lookup_AddLookupStep(&$res, $sub, $method) {
  243. $res['LookupSteps'][] = $sub;
  244. $property = $sub['Call']['Method']['text'];
  245. if (isset($sub['Call']['CallArguments']) && $arguments = $sub['Call']['CallArguments']['php']) {
  246. $res['php'] .= "->$method('$property', array($arguments), true)";
  247. }
  248. else {
  249. $res['php'] .= "->$method('$property', null, true)";
  250. }
  251. }
  252. function Lookup_LookupStep(&$res, $sub) {
  253. $this->Lookup_AddLookupStep($res, $sub, 'obj');
  254. }
  255. function Lookup_LastLookupStep(&$res, $sub) {
  256. $this->Lookup_AddLookupStep($res, $sub, '$$FINAL');
  257. }
  258. /*!*
  259. # New Translatable Syntax
  260. # <%t Entity DefaultString is Context name1=string name2=$functionCall
  261. # (This is a new way to call translatable strings. The parser transforms this into a call to the _t() method)
  262. Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? <
  263. (InjectionVariables)? > "%>"
  264. InjectionVariables: (< InjectionName:Word "=" Argument)+
  265. Entity: / [A-Za-z_] [\w\.]* /
  266. */
  267. function Translate__construct(&$res) {
  268. $res['php'] = '$val .= _t(';
  269. }
  270. function Translate_Entity(&$res, $sub) {
  271. $res['php'] .= "'$sub[text]'";
  272. }
  273. function Translate_Default(&$res, $sub) {
  274. $res['php'] .= ",$sub[text]";
  275. }
  276. function Translate_Context(&$res, $sub) {
  277. $res['php'] .= ",$sub[text]";
  278. }
  279. function Translate_InjectionVariables(&$res, $sub) {
  280. $res['php'] .= ",$sub[php]";
  281. }
  282. function Translate__finalise(&$res) {
  283. $res['php'] .= ');';
  284. }
  285. function InjectionVariables__construct(&$res) {
  286. $res['php'] = "array(";
  287. }
  288. function InjectionVariables_InjectionName(&$res, $sub) {
  289. $res['php'] .= "'$sub[text]'=>";
  290. }
  291. function InjectionVariables_Argument(&$res, $sub) {
  292. $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']) . ',';
  293. }
  294. function InjectionVariables__finalise(&$res) {
  295. if (substr($res['php'], -1) == ',') $res['php'] = substr($res['php'], 0, -1); //remove last comma in the array
  296. $res['php'] .= ')';
  297. }
  298. /*!*
  299. # Injections are where, outside of a block, a value needs to be inserted into the output. You can either
  300. # just do $Foo, or {$Foo} if the surrounding text would cause a problem (e.g. {$Foo}Bar)
  301. SimpleInjection: '$' :Lookup
  302. BracketInjection: '{$' :Lookup "}"
  303. Injection: BracketInjection | SimpleInjection
  304. */
  305. function Injection_STR(&$res, $sub) {
  306. $res['php'] = '$val .= '. str_replace('$$FINAL', 'XML_val', $sub['Lookup']['php']) . ';';
  307. }
  308. /*!*
  309. # Inside a block's arguments you can still use the same format as a simple injection ($Foo). In this case
  310. # it marks the argument as being a lookup, not a string (if it was bare it might still be used as a lookup,
  311. # but that depends on where it's used, a la 2.4)
  312. DollarMarkedLookup: SimpleInjection
  313. */
  314. function DollarMarkedLookup_STR(&$res, $sub) {
  315. $res['Lookup'] = $sub['Lookup'];
  316. }
  317. /*!*
  318. # Inside a block's arguments you can explictly mark a string by surrounding it with quotes (single or double,
  319. # but they must be matching). If you do, inside the quote you can escape any character, but the only character
  320. # that _needs_ escaping is the matching closing quote
  321. QuotedString: q:/['"]/ String:/ (\\\\ | \\. | [^$q\\])* / '$q'
  322. # In order to support 2.4's base syntax, we also need to detect free strings - strings not surrounded by
  323. # quotes, and containing spaces or punctuation, but supported as a single string. We support almost as flexible
  324. # a string as 2.4 - we don't attempt to determine the closing character by context, but just break on any
  325. # character which, in some context, would indicate the end of a free string, regardless of if we're actually in
  326. # that context or not
  327. FreeString: /[^,)%!=><|&]+/
  328. # An argument - either a marked value, or a bare value, prefering lookup matching on the bare value over
  329. # freestring matching as long as that would give a successful parse
  330. Argument:
  331. :DollarMarkedLookup |
  332. :QuotedString |
  333. :Lookup !(< FreeString)|
  334. :FreeString
  335. */
  336. /**
  337. * If we get a bare value, we don't know enough to determine exactly what php would be the translation, because
  338. * we don't know if the position of use indicates a lookup or a string argument.
  339. *
  340. * Instead, we record 'ArgumentMode' as a member of this matches results node, which can be:
  341. * - lookup if this argument was unambiguously a lookup (marked as such)
  342. * - string is this argument was unambiguously a string (marked as such, or impossible to parse as lookup)
  343. * - default if this argument needs to be handled as per 2.4
  344. *
  345. * In the case of 'default', there is no php member of the results node, but instead 'lookup_php', which
  346. * should be used by the parent if the context indicates a lookup, and 'string_php' which should be used
  347. * if the context indicates a string
  348. */
  349. function Argument_DollarMarkedLookup(&$res, $sub) {
  350. $res['ArgumentMode'] = 'lookup';
  351. $res['php'] = $sub['Lookup']['php'];
  352. }
  353. function Argument_QuotedString(&$res, $sub) {
  354. $res['ArgumentMode'] = 'string';
  355. $res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'";
  356. }
  357. function Argument_Lookup(&$res, $sub) {
  358. if (count($sub['LookupSteps']) == 1 && !isset($sub['LookupSteps'][0]['Call']['Arguments'])) {
  359. $res['ArgumentMode'] = 'default';
  360. $res['lookup_php'] = $sub['php'];
  361. $res['string_php'] = "'".$sub['LookupSteps'][0]['Call']['Method']['text']."'";
  362. }
  363. else {
  364. $res['ArgumentMode'] = 'lookup';
  365. $res['php'] = $sub['php'];
  366. }
  367. }
  368. function Argument_FreeString(&$res, $sub) {
  369. $res['ArgumentMode'] = 'string';
  370. $res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'";
  371. }
  372. /*!*
  373. # if and else_if blocks allow basic comparisons between arguments
  374. ComparisonOperator: "!=" | "==" | ">=" | ">" | "<=" | "<" | "="
  375. Comparison: Argument < ComparisonOperator > Argument
  376. */
  377. function Comparison_Argument(&$res, $sub) {
  378. if ($sub['ArgumentMode'] == 'default') {
  379. if (!empty($res['php'])) $res['php'] .= $sub['string_php'];
  380. else $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']);
  381. }
  382. else {
  383. $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']);
  384. }
  385. }
  386. function Comparison_ComparisonOperator(&$res, $sub) {
  387. $res['php'] .= ($sub['text'] == '=' ? '==' : $sub['text']);
  388. }
  389. /*!*
  390. # If a comparison operator is not used in an if or else_if block, then the statement is a 'presence check',
  391. # which checks if the argument given is present or not. For explicit strings (which were not allowed in 2.4)
  392. # this falls back to simple truthiness check
  393. PresenceCheck: (Not:'not' <)? Argument
  394. */
  395. function PresenceCheck_Not(&$res, $sub) {
  396. $res['php'] = '!';
  397. }
  398. function PresenceCheck_Argument(&$res, $sub) {
  399. if ($sub['ArgumentMode'] == 'string') {
  400. $res['php'] .= '((bool)'.$sub['php'].')';
  401. }
  402. else {
  403. $php = ($sub['ArgumentMode'] == 'default' ? $sub['lookup_php'] : $sub['php']);
  404. // TODO: kinda hacky - maybe we need a way to pass state down the parse chain so
  405. // Lookup_LastLookupStep and Argument_BareWord can produce hasValue instead of XML_val
  406. $res['php'] .= str_replace('$$FINAL', 'hasValue', $php);
  407. }
  408. }
  409. /*!*
  410. # if and else_if arguments are a series of presence checks and comparisons, optionally seperated by boolean
  411. # operators
  412. IfArgumentPortion: Comparison | PresenceCheck
  413. */
  414. function IfArgumentPortion_STR(&$res, $sub) {
  415. $res['php'] = $sub['php'];
  416. }
  417. /*!*
  418. # if and else_if arguments can be combined via these two boolean operators. No precendence overriding is
  419. # supported
  420. BooleanOperator: "||" | "&&"
  421. # This is the combination of the previous if and else_if argument portions
  422. IfArgument: :IfArgumentPortion ( < :BooleanOperator < :IfArgumentPortion )*
  423. */
  424. function IfArgument_IfArgumentPortion(&$res, $sub) {
  425. $res['php'] .= $sub['php'];
  426. }
  427. function IfArgument_BooleanOperator(&$res, $sub) {
  428. $res['php'] .= $sub['text'];
  429. }
  430. /*!*
  431. # ifs are handled seperately from other closed block tags, because (A) their structure is different - they
  432. # can have else_if and else tags in between the if tag and the end_if tag, and (B) they have a different
  433. # argument structure to every other block
  434. IfPart: '<%' < 'if' [ :IfArgument > '%>' Template:$TemplateMatcher?
  435. ElseIfPart: '<%' < 'else_if' [ :IfArgument > '%>' Template:$TemplateMatcher?
  436. ElsePart: '<%' < 'else' > '%>' Template:$TemplateMatcher?
  437. If: IfPart ElseIfPart* ElsePart? '<%' < 'end_if' > '%>'
  438. */
  439. function If_IfPart(&$res, $sub) {
  440. $res['php'] =
  441. 'if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL .
  442. (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
  443. '}';
  444. }
  445. function If_ElseIfPart(&$res, $sub) {
  446. $res['php'] .=
  447. 'else if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL .
  448. (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
  449. '}';
  450. }
  451. function If_ElsePart(&$res, $sub) {
  452. $res['php'] .=
  453. 'else { ' . PHP_EOL .
  454. (isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
  455. '}';
  456. }
  457. /*!*
  458. # The require block is handled seperately to the other open blocks as the argument syntax is different
  459. # - must have one call style argument, must pass arguments to that call style argument
  460. Require: '<%' < 'require' [ Call:(Method:Word "(" < :CallArguments > ")") > '%>'
  461. */
  462. function Require_Call(&$res, $sub) {
  463. $res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');';
  464. }
  465. /*!*
  466. # Cache block arguments don't support free strings
  467. CacheBlockArgument:
  468. !( "if " | "unless " )
  469. (
  470. :DollarMarkedLookup |
  471. :QuotedString |
  472. :Lookup
  473. )
  474. */
  475. function CacheBlockArgument_DollarMarkedLookup(&$res, $sub) {
  476. $res['php'] = $sub['Lookup']['php'];
  477. }
  478. function CacheBlockArgument_QuotedString(&$res, $sub) {
  479. $res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'";
  480. }
  481. function CacheBlockArgument_Lookup(&$res, $sub) {
  482. $res['php'] = $sub['php'];
  483. }
  484. /*!*
  485. # Collects the arguments passed in to be part of the key of a cacheblock
  486. CacheBlockArguments: CacheBlockArgument ( < "," < CacheBlockArgument )*
  487. */
  488. function CacheBlockArguments_CacheBlockArgument(&$res, $sub) {
  489. if (!empty($res['php'])) $res['php'] .= ".'_'.";
  490. else $res['php'] = '';
  491. $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']);
  492. }
  493. /*!*
  494. # CacheBlockTemplate is the same as Template, but doesn't include cache blocks (because they're handled seperately)
  495. CacheBlockTemplate extends Template (TemplateMatcher = CacheRestrictedTemplate); CacheBlock | UncachedBlock | => ''
  496. */
  497. /*!*
  498. UncachedBlock:
  499. '<%' < "uncached" < CacheBlockArguments? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>'
  500. Template:$TemplateMatcher?
  501. '<%' < 'end_' ("uncached"|"cached"|"cacheblock") > '%>'
  502. */
  503. function UncachedBlock_Template(&$res, $sub){
  504. $res['php'] = $sub['php'];
  505. }
  506. /*!*
  507. # CacheRestrictedTemplate is the same as Template, but doesn't allow cache blocks
  508. CacheRestrictedTemplate extends Template
  509. */
  510. function CacheRestrictedTemplate_CacheBlock(&$res, $sub) {
  511. throw new SSTemplateParseException('You cant have cache blocks nested within with, loop or control blocks ' .
  512. 'that are within cache blocks', $this);
  513. }
  514. function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) {
  515. throw new SSTemplateParseException('You cant have uncache blocks nested within with, loop or control blocks ' .
  516. 'that are within cache blocks', $this);
  517. }
  518. /*!*
  519. # The partial caching block
  520. CacheBlock:
  521. '<%' < CacheTag:("cached"|"cacheblock") < (CacheBlockArguments)? ( < Conditional:("if"|"unless") >
  522. Condition:IfArgument )? > '%>'
  523. (CacheBlock | UncachedBlock | CacheBlockTemplate)*
  524. '<%' < 'end_' ("cached"|"uncached"|"cacheblock") > '%>'
  525. */
  526. function CacheBlock__construct(&$res){
  527. $res['subblocks'] = 0;
  528. }
  529. function CacheBlock_CacheBlockArguments(&$res, $sub){
  530. $res['key'] = !empty($sub['php']) ? $sub['php'] : '';
  531. }
  532. function CacheBlock_Condition(&$res, $sub){
  533. $res['condition'] = ($res['Conditional']['text'] == 'if' ? '(' : '!(') . $sub['php'] . ') && ';
  534. }
  535. function CacheBlock_CacheBlock(&$res, $sub){
  536. $res['php'] .= $sub['php'];
  537. }
  538. function CacheBlock_UncachedBlock(&$res, $sub){
  539. $res['php'] .= $sub['php'];
  540. }
  541. function CacheBlock_CacheBlockTemplate(&$res, $sub){
  542. // Get the block counter
  543. $block = ++$res['subblocks'];
  544. // Build the key for this block from the global key (evaluated in a closure within the template),
  545. // the passed cache key, the block index, and the sha hash of the template.
  546. $res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
  547. $res['php'] .= '$val = \'\';' . PHP_EOL;
  548. if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
  549. // Embed the code necessary to evaluate the globalKey directly into the template,
  550. // so that SSTemplateParser only needs to be called during template regeneration.
  551. // Warning: If the global key is changed, it's necessary to flush the template cache.
  552. $parser = Injector::inst()->get('SSTemplateParser', false);
  553. $result = $parser->compileString($globalKey, '', false, false);
  554. if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
  555. $res['php'] .= $result . PHP_EOL;
  556. }
  557. $res['php'] .= 'return $val;' . PHP_EOL;
  558. $res['php'] .= '};' . PHP_EOL;
  559. $key = 'sha1($keyExpression())' // Global key
  560. . '.\'_' . sha1($sub['php']) // sha of template
  561. . (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
  562. . ".'_$block'"; // block index
  563. // Get any condition
  564. $condition = isset($res['condition']) ? $res['condition'] : '';
  565. $res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL;
  566. $res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL;
  567. $res['php'] .= $sub['php'] . PHP_EOL;
  568. $res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL;
  569. $res['php'] .= '}';
  570. }
  571. /*!*
  572. # Deprecated old-style i18n _t and sprintf(_t block tags. We support a slightly more flexible version than we used
  573. # to, but just because it's easier to do so. It's strongly recommended to use the new syntax
  574. # This is the core used by both syntaxes, without the block start & end tags
  575. OldTPart: "_t" N "(" N QuotedString (N "," N CallArguments)? N ")" N (";")?
  576. # whitespace with a newline
  577. N: / [\s\n]* /
  578. */
  579. function OldTPart__construct(&$res) {
  580. $res['php'] = "_t(";
  581. }
  582. function OldTPart_QuotedString(&$res, $sub) {
  583. $entity = $sub['String']['text'];
  584. if (strpos($entity, '.') === false) {
  585. $res['php'] .= "\$scope->XML_val('I18NNamespace').'.$entity'";
  586. }
  587. else {
  588. $res['php'] .= "'$entity'";
  589. }
  590. }
  591. function OldTPart_CallArguments(&$res, $sub) {
  592. $res['php'] .= ',' . $sub['php'];
  593. }
  594. function OldTPart__finalise(&$res) {
  595. $res['php'] .= ')';
  596. }
  597. /*!*
  598. # This is the old <% _t() %> tag
  599. OldTTag: "<%" < OldTPart > "%>"
  600. */
  601. function OldTTag_OldTPart(&$res, $sub) {
  602. $res['php'] = $sub['php'];
  603. }
  604. /*!*
  605. # This is the old <% sprintf(_t()) %> tag
  606. OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>"
  607. */
  608. function OldSprintfTag__construct(&$res) {
  609. $res['php'] = "sprintf(";
  610. }
  611. function OldSprintfTag_OldTPart(&$res, $sub) {
  612. $res['php'] .= $sub['php'];
  613. }
  614. function OldSprintfTag_CallArguments(&$res, $sub) {
  615. $res['php'] .= ',' . $sub['php'] . ')';
  616. }
  617. /*!*
  618. # This matches either the old style sprintf(_t()) or _t() tags. As well as including the output portion of the
  619. # php, this rule combines all the old i18n stuff into a single match rule to make it easy to not support these
  620. # tags later
  621. OldI18NTag: OldSprintfTag | OldTTag
  622. */
  623. function OldI18NTag_STR(&$res, $sub) {
  624. $res['php'] = '$val .= ' . $sub['php'] . ';';
  625. }
  626. /*!*
  627. # An argument that can be passed through to an included template
  628. NamedArgument: Name:Word "=" Value:Argument
  629. */
  630. function NamedArgument_Name(&$res, $sub) {
  631. $res['php'] = "'" . $sub['text'] . "' => ";
  632. }
  633. function NamedArgument_Value(&$res, $sub) {
  634. switch($sub['ArgumentMode']) {
  635. case 'string':
  636. $res['php'] .= $sub['php'];
  637. break;
  638. case 'default':
  639. $res['php'] .= $sub['string_php'];
  640. break;
  641. default:
  642. $res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
  643. break;
  644. }
  645. }
  646. /*!*
  647. # The include tag
  648. Include: "<%" < "include" < Template:NamespacedWord < (NamedArgument ( < "," < NamedArgument )*)? > "%>"
  649. */
  650. function Include__construct(&$res){
  651. $res['arguments'] = array();
  652. }
  653. function Include_Template(&$res, $sub){
  654. $res['template'] = "'" . $sub['text'] . "'";
  655. }
  656. function Include_NamedArgument(&$res, $sub){
  657. $res['arguments'][] = $sub['php'];
  658. }
  659. function Include__finalise(&$res){
  660. $template = $res['template'];
  661. $arguments = $res['arguments'];
  662. $res['php'] = '$val .= SSViewer::execute_template(["type" => "Includes", '.$template.'], $scope->getItem(), array(' .
  663. implode(',', $arguments)."), \$scope);\n";
  664. if($this->includeDebuggingComments) { // Add include filename comments on dev sites
  665. $res['php'] =
  666. '$val .= \'<!-- include '.addslashes($template).' -->\';'. "\n".
  667. $res['php'].
  668. '$val .= \'<!-- end include '.addslashes($template).' -->\';'. "\n";
  669. }
  670. }
  671. /*!*
  672. # To make the block support reasonably extendable, we don't explicitly define each closed block and it's structure,
  673. # but instead match against a generic <% block_name argument, ... %> pattern. Each argument is left as per the
  674. # output of the Argument matcher, and the handler (see the PHPDoc block later for more on this) is responsible
  675. # for pulling out the info required
  676. BlockArguments: :Argument ( < "," < :Argument)*
  677. # NotBlockTag matches against any word that might come after a "<%" that the generic open and closed block handlers
  678. # shouldn't attempt to match against, because they're handled by more explicit matchers
  679. NotBlockTag: "end_" | (("if" | "else_if" | "else" | "require" | "cached" | "uncached" | "cacheblock" | "include")])
  680. # Match against closed blocks - blocks with an opening and a closing tag that surround some internal portion of
  681. # template
  682. ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher?
  683. '<%' < 'end_' '$BlockName' > '%>'
  684. */
  685. /**
  686. * As mentioned in the parser comment, block handling is kept fairly generic for extensibility. The match rule
  687. * builds up two important elements in the match result array:
  688. * 'ArgumentCount' - how many arguments were passed in the opening tag
  689. * 'Arguments' an array of the Argument match rule result arrays
  690. *
  691. * Once a block has successfully been matched against, it will then look for the actual handler, which should
  692. * be on this class (either defined or extended on) as ClosedBlock_Handler_Name(&$res), where Name is the
  693. * tag name, first letter captialized (i.e Control, Loop, With, etc).
  694. *
  695. * This function will be called with the match rule result array as it's first argument. It should return
  696. * the php result of this block as it's return value, or throw an error if incorrect arguments were passed.
  697. */
  698. function ClosedBlock__construct(&$res) {
  699. $res['ArgumentCount'] = 0;
  700. }
  701. function ClosedBlock_BlockArguments(&$res, $sub) {
  702. if (isset($sub['Argument']['ArgumentMode'])) {
  703. $res['Arguments'] = array($sub['Argument']);
  704. $res['ArgumentCount'] = 1;
  705. }
  706. else {
  707. $res['Arguments'] = $sub['Argument'];
  708. $res['ArgumentCount'] = count($res['Arguments']);
  709. }
  710. }
  711. function ClosedBlock__finalise(&$res) {
  712. $blockname = $res['BlockName']['text'];
  713. $method = 'ClosedBlock_Handle_'.$blockname;
  714. if (method_exists($this, $method)) {
  715. $res['php'] = $this->$method($res);
  716. } else if (isset($this->closedBlocks[$blockname])) {
  717. $res['php'] = call_user_func($this->closedBlocks[$blockname], $res);
  718. } else {
  719. throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
  720. 'not supposed to close this block, or have mis-spelled it?', $this);
  721. }
  722. }
  723. /**
  724. * This is an example of a block handler function. This one handles the loop tag.
  725. */
  726. function ClosedBlock_Handle_Loop(&$res) {
  727. if ($res['ArgumentCount'] > 1) {
  728. throw new SSTemplateParseException('Either no or too many arguments in control block. Must be one ' .
  729. 'argument only.', $this);
  730. }
  731. //loop without arguments loops on the current scope
  732. if ($res['ArgumentCount'] == 0) {
  733. $on = '$scope->obj(\'Up\', null)->obj(\'Foo\', null)';
  734. } else { //loop in the normal way
  735. $arg = $res['Arguments'][0];
  736. if ($arg['ArgumentMode'] == 'string') {
  737. throw new SSTemplateParseException('Control block cant take string as argument.', $this);
  738. }
  739. $on = str_replace('$$FINAL', 'obj',
  740. ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']);
  741. }
  742. return
  743. $on . '; $scope->pushScope(); while (($key = $scope->next()) !== false) {' . PHP_EOL .
  744. $res['Template']['php'] . PHP_EOL .
  745. '}; $scope->popScope(); ';
  746. }
  747. /**
  748. * The closed block handler for with blocks
  749. */
  750. function ClosedBlock_Handle_With(&$res) {
  751. if ($res['ArgumentCount'] != 1) {
  752. throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one ' .
  753. 'argument only.', $this);
  754. }
  755. $arg = $res['Arguments'][0];
  756. if ($arg['ArgumentMode'] == 'string') {
  757. throw new SSTemplateParseException('Control block cant take string as argument.', $this);
  758. }
  759. $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']);
  760. return
  761. $on . '; $scope->pushScope();' . PHP_EOL .
  762. $res['Template']['php'] . PHP_EOL .
  763. '; $scope->popScope(); ';
  764. }
  765. /*!*
  766. # Open blocks are handled in the same generic manner as closed blocks. There is no need to define which blocks
  767. # are which - closed is tried first, and if no matching end tag is found, open is tried next
  768. OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>'
  769. */
  770. function OpenBlock__construct(&$res) {
  771. $res['ArgumentCount'] = 0;
  772. }
  773. function OpenBlock_BlockArguments(&$res, $sub) {
  774. if (isset($sub['Argument']['ArgumentMode'])) {
  775. $res['Arguments'] = array($sub['Argument']);
  776. $res['ArgumentCount'] = 1;
  777. }
  778. else {
  779. $res['Arguments'] = $sub['Argument'];
  780. $res['ArgumentCount'] = count($res['Arguments']);
  781. }
  782. }
  783. function OpenBlock__finalise(&$res) {
  784. $blockname = $res['BlockName']['text'];
  785. $method = 'OpenBlock_Handle_'.$blockname;
  786. if (method_exists($this, $method)) {
  787. $res['php'] = $this->$method($res);
  788. } elseif (isset($this->openBlocks[$blockname])) {
  789. $res['php'] = call_user_func($this->openBlocks[$blockname], $res);
  790. } else {
  791. throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
  792. ' the closing tag or have mis-spelled it?', $this);
  793. }
  794. }
  795. /**
  796. * This is an open block handler, for the <% debug %> utility tag
  797. */
  798. function OpenBlock_Handle_Debug(&$res) {
  799. if ($res['ArgumentCount'] == 0) return '$scope->debug();';
  800. else if ($res['ArgumentCount'] == 1) {
  801. $arg = $res['Arguments'][0];
  802. if ($arg['ArgumentMode'] == 'string') return 'Debug::show('.$arg['php'].');';
  803. $php = ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php'];
  804. return '$val .= Debug::show('.str_replace('FINALGET!', 'cachedCall', $php).');';
  805. }
  806. else {
  807. throw new SSTemplateParseException('Debug takes 0 or 1 argument only.', $this);
  808. }
  809. }
  810. /**
  811. * This is an open block handler, for the <% base_tag %> tag
  812. */
  813. function OpenBlock_Handle_Base_tag(&$res) {
  814. if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Base_tag takes no arguments', $this);
  815. return '$val .= SSViewer::get_base_tag($val);';
  816. }
  817. /**
  818. * This is an open block handler, for the <% current_page %> tag
  819. */
  820. function OpenBlock_Handle_Current_page(&$res) {
  821. if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Current_page takes no arguments', $this);
  822. return '$val .= $_SERVER[SCRIPT_URL];';
  823. }
  824. /*!*
  825. # This is used to detect when we have a mismatched closing tag (i.e., one with no equivilent opening tag)
  826. # Because of parser limitations, this can only be used at the top nesting level of a template. Other mismatched
  827. # closing tags are detected as an invalid open tag
  828. MismatchedEndBlock: '<%' < 'end_' :Word > '%>'
  829. */
  830. function MismatchedEndBlock__finalise(&$res) {
  831. $blockname = $res['Word']['text'];
  832. throw new SSTemplateParseException('Unexpected close tag end_' . $blockname .
  833. ' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
  834. }
  835. /*!*
  836. # This is used to detect a malformed opening tag - one where the tag is opened with the "<%" characters, but
  837. # the tag is not structured properly
  838. MalformedOpenTag: '<%' < !NotBlockTag Tag:Word !( ( [ :BlockArguments ] )? > '%>' )
  839. */
  840. function MalformedOpenTag__finalise(&$res) {
  841. $tag = $res['Tag']['text'];
  842. throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?"
  843. , $this);
  844. }
  845. /*!*
  846. # This is used to detect a malformed end tag - one where the tag is opened with the "<%" characters, but
  847. # the tag is not structured properly
  848. MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' )
  849. */
  850. function MalformedCloseTag__finalise(&$res) {
  851. $tag = $res['Tag']['text'];
  852. throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " .
  853. "argument to one?", $this);
  854. }
  855. /*!*
  856. # This is used to detect a malformed tag. It's mostly to keep the Template match rule a bit shorter
  857. MalformedBlock: MalformedOpenTag | MalformedCloseTag
  858. */
  859. /*!*
  860. # This is used to remove template comments
  861. Comment: "<%--" (!"--%>" /(?s)./)+ "--%>"
  862. */
  863. function Comment__construct(&$res) {
  864. $res['php'] = '';
  865. }
  866. /*!*
  867. # TopTemplate is the same as Template, but should only be used at the top level (not nested), as it includes
  868. # MismatchedEndBlock detection, which only works at the top level
  869. TopTemplate extends Template (TemplateMatcher = Template); MalformedBlock => MalformedBlock | MismatchedEndBlock
  870. */
  871. /**
  872. * The TopTemplate also includes the opening stanza to start off the template
  873. */
  874. function TopTemplate__construct(&$res) {
  875. $res['php'] = "<?php" . PHP_EOL;
  876. }
  877. /*!*
  878. # Text matches anything that isn't a template command (not an injection, block of any kind or comment)
  879. Text: (
  880. # Any set of characters that aren't potentially a control mark or an escaped character
  881. / [^<${\\]+ / |
  882. # An escaped character
  883. / (\\.) / |
  884. # A '<' that isn't the start of a block tag
  885. '<' !'%' |
  886. # A '$' that isn't the start of an injection
  887. '$' !(/[A-Za-z_]/) |
  888. # A '{' that isn't the start of an injection
  889. '{' !'$' |
  890. # A '{$' that isn't the start of an injection
  891. '{$' !(/[A-Za-z_]/)
  892. )+
  893. */
  894. /**
  895. * We convert text
  896. */
  897. function Text__finalise(&$res) {
  898. $text = $res['text'];
  899. // Unescape any escaped characters in the text, then put back escapes for any single quotes and backslashes
  900. $text = stripslashes($text);
  901. $text = addcslashes($text, '\'\\');
  902. // TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
  903. // non-dynamically calculated
  904. $code = <<<'EOC'
  905. (\Config::inst()->get('SSViewer', 'rewrite_hash_links')
  906. ? \Convert::raw2att( preg_replace("/^(\\/)+/", "/", $_SERVER['REQUEST_URI'] ) )
  907. : "")
  908. EOC;
  909. // Because preg_replace replacement requires escaped slashes, addcslashes here
  910. $text = preg_replace(
  911. '/(<a[^>]+href *= *)"#/i',
  912. '\\1"\' . ' . addcslashes($code, '\\') . ' . \'#',
  913. $text
  914. );
  915. $res['php'] .= '$val .= \'' . $text . '\';' . PHP_EOL;
  916. }
  917. /******************
  918. * Here ends the parser itself. Below are utility methods to use the parser
  919. */
  920. /**
  921. * Compiles some passed template source code into the php code that will execute as per the template source.
  922. *
  923. * @throws SSTemplateParseException
  924. * @param $string The source of the template
  925. * @param string $templateName The name of the template, normally the filename the template source was loaded from
  926. * @param bool $includeDebuggingComments True is debugging comments should be included in the output
  927. * @param bool $topTemplate True if this is a top template, false if it's just a template
  928. * @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
  929. */
  930. public function compileString($string, $templateName = "", $includeDebuggingComments=false, $topTemplate = true) {
  931. if (!trim($string)) {
  932. $code = '';
  933. }
  934. else {
  935. parent::__construct($string);
  936. $this->includeDebuggingComments = $includeDebuggingComments;
  937. // Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
  938. // (and other encodings) properly
  939. if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3;
  940. // Match the source against the parser
  941. if ($topTemplate) {
  942. $result = $this->match_TopTemplate();
  943. } else {
  944. $result = $this->match_Template();
  945. }
  946. if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this);
  947. // Get the result
  948. $code = $result['php'];
  949. }
  950. // Include top level debugging comments if desired
  951. if($includeDebuggingComments && $templateName && stripos($code, "<?xml") === false) {
  952. $code = $this->includeDebuggingComments($code, $templateName);
  953. }
  954. return $code;
  955. }
  956. /**
  957. * @param string $code
  958. * @return string $code
  959. */
  960. protected function includeDebuggingComments($code, $templateName) {
  961. // If this template contains a doctype, put it right after it,
  962. // if not, put it after the <html> tag to avoid IE glitches
  963. if(stripos($code, "<!doctype") !== false) {
  964. $code = preg_replace('/(<!doctype[^>]*("[^"]")*[^>]*>)/im', "$1\r\n<!-- template $templateName -->", $code);
  965. $code .= "\r\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
  966. } elseif(stripos($code, "<html") !== false) {
  967. $code = preg_replace_callback('/(.*)(<html[^>]*>)(.*)/i', function($matches) use ($templateName) {
  968. if (stripos($matches[3], '<!--') === false && stripos($matches[3], '-->') !== false) {
  969. // after this <html> tag there is a comment close but no comment has been opened
  970. // this most likely means that this <html> tag is inside a comment
  971. // we should not add a comment inside a comment (invalid html)
  972. // lets append it at the end of the comment
  973. // an example case for this is the html5boilerplate: <!--[if IE]><html class="ie"><![endif]-->
  974. return $matches[0];
  975. } else {
  976. // all other cases, add the comment and return it
  977. return "{$matches[1]}{$matches[2]}<!-- template $templateName -->{$matches[3]}";
  978. }
  979. }, $code);
  980. $code = preg_replace('/(<\/html[^>]*>)/i', "<!-- end template $templateName -->$1", $code);
  981. } else {
  982. $code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName .
  983. ' -->\';' . "\r\n", $code);
  984. $code .= "\r\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
  985. }
  986. return $code;
  987. }
  988. /**
  989. * Compiles some file that contains template source code, and returns the php code that will execute as per that
  990. * source
  991. *
  992. * @static
  993. * @param $template - A file path that contains template source code
  994. * @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
  995. */
  996. public function compileFile($template) {
  997. return $this->compileString(file_get_contents($template), $template);
  998. }
  999. }