SSTemplateParser.php.inc 40.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240
<?php

/*!* !insert_autogen_warning */

/*!* !silent
This is the uncompiled parser for the SilverStripe template language, PHP with special comments that define the
parser. 

It gets run through the php-peg parser compiler to have those comments turned into code that match parts of the 
template language, producing the executable version SSTemplateParser.php

To recompile after changing this file, run this from the 'framework/view' directory via command line (in most cases
this is: sapphire/view):
 
	php ../thirdparty/php-peg/cli.php SSTemplateParser.php.inc > SSTemplateParser.php 
  
See the php-peg docs for more information on the parser format, and how to convert this file into SSTemplateParser.php

TODO:
	Template comments - <%-- --%>
	$Iteration
	Partial cache blocks
	i18n - we dont support then deprecated _t() or sprintf(_t()) methods; or the new <% t %> block yet
	Add with and loop blocks
	Add Up and Top 
	More error detection?
 
This comment will not appear in the output
*/

// We want this to work when run by hand too
if (defined(THIRDPARTY_PATH)) {
	require_once(THIRDPARTY_PATH . '/php-peg/Parser.php');
}
else {
	$base = dirname(__FILE__);
	require_once($base.'/../thirdparty/php-peg/Parser.php');
}

/**
 * This is the exception raised when failing to parse a template. Note that we don't currently do any static analysis,
 * so we can't know if the template will run, just if it's malformed. It also won't catch mistakes that still look
 * valid.
 *
 * @package framework
 * @subpackage view
 */
class SSTemplateParseException extends Exception {
	
	function __construct($message, $parser) {
		$prior = substr($parser->string, 0, $parser->pos);
		
		preg_match_all('/\r\n|\r|\n/', $prior, $matches);
		$line = count($matches[0])+1;
		
		parent::__construct("Parse error in template on line $line. Error was: $message");
	}
	
}

/**
  * This is the parser for the SilverStripe template language. It gets called on a string and uses a php-peg parser
  * to match that string against the language structure, building up the PHP code to execute that structure as it
  * parses
  * 
  * The $result array that is built up as part of the parsing (see thirdparty/php-peg/README.md for more on how
  * parsers build results) has one special member, 'php', which contains the php equivalent of that part of the
  * template tree.
  * 
  * Some match rules generate alternate php, or other variations, so check the per-match documentation too.
  * 
  * Terms used:
  * 
  * Marked: A string or lookup in the template that has been explictly marked as such - lookups by prepending with
  * "$" (like $Foo.Bar), strings by wrapping with single or double quotes ('Foo' or "Foo")
  * 
  * Bare: The opposite of marked. An argument that has to has it's type inferred by usage and 2.4 defaults.
  * 
  * Example of using a bare argument for a loop block: <% loop Foo %>
  * 
  * Block: One of two SS template structures. The special characters "<%" and "%>" are used to wrap the opening and
  * (required or forbidden depending on which block exactly) closing block marks.
  * 
  * Open Block: An SS template block that doesn't wrap any content or have a closing end tag (in fact, a closing end
  * tag is forbidden)
  * 
  * Closed Block: An SS template block that wraps content, and requires a counterpart <% end_blockname %> tag
  * 
  * Angle Bracket: angle brackets "<" and ">" are used to eat whitespace between template elements
  * N: eats white space including newlines (using in legacy _t support)
  *
  * @package framework
  * @subpackage view
  */
class SSTemplateParser extends Parser implements TemplateParser {

	/**
	 * @var bool - Set true by SSTemplateParser::compileString if the template should include comments intended
	 * for debugging (template source, included files, etc)
	 */
	protected $includeDebuggingComments = false;

	/**
	 * Stores the user-supplied closed block extension rules in the form:
	 * array(
	 *   'name' => function (&$res) {}
	 * )
	 * See SSTemplateParser::ClosedBlock_Handle_Loop for an example of what the callable should look like
	 * @var array
	 */
	protected $closedBlocks = array();

	/**
	 * Stores the user-supplied open block extension rules in the form:
	 * array(
	 *   'name' => function (&$res) {}
	 * )
	 * See SSTemplateParser::OpenBlock_Handle_Base_tag for an example of what the callable should look like
	 * @var array
	 */
	protected $openBlocks = array();

	/**
	 * Allow the injection of new closed & open block callables
	 * @param array $closedBlocks
	 * @param array $openBlocks
	 */
	public function __construct($closedBlocks = array(), $openBlocks = array()) {
		$this->setClosedBlocks($closedBlocks);
		$this->setOpenBlocks($openBlocks);
	}

	/**
	 * Override the function that constructs the result arrays to also prepare a 'php' item in the array
	 */
	function construct($matchrule, $name, $arguments = null) {
		$res = parent::construct($matchrule, $name, $arguments);
		if (!isset($res['php'])) $res['php'] = '';
		return $res;
	}

	/**
	 * Set the closed blocks that the template parser should use
	 * 
	 * This method will delete any existing closed blocks, please use addClosedBlock if you don't
	 * want to overwrite
	 * @param array $closedBlocks
	 * @throws InvalidArgumentException
	 */
	public function setClosedBlocks($closedBlocks) {
		$this->closedBlocks = array();
		foreach ((array) $closedBlocks as $name => $callable) {
			$this->addClosedBlock($name, $callable);
		}
	}

	/**
	 * Set the open blocks that the template parser should use
	 *
	 * This method will delete any existing open blocks, please use addOpenBlock if you don't
	 * want to overwrite
	 * @param array $openBlocks
	 * @throws InvalidArgumentException
	 */
	public function setOpenBlocks($openBlocks) {
		$this->openBlocks = array();
		foreach ((array) $openBlocks as $name => $callable) {
			$this->addOpenBlock($name, $callable);
		}
	}

	/**
	 * Add a closed block callable to allow <% name %><% end_name %> syntax
	 * @param string $name The name of the token to be used in the syntax <% name %><% end_name %>
	 * @param callable $callable The function that modifies the generation of template code
	 * @throws InvalidArgumentException
	 */
	public function addClosedBlock($name, $callable) {
		$this->validateExtensionBlock($name, $callable, 'Closed block');
		$this->closedBlocks[$name] = $callable;
	}

	/**
	 * Add a closed block callable to allow <% name %> syntax
	 * @param string $name The name of the token to be used in the syntax <% name %>
	 * @param callable $callable The function that modifies the generation of template code
	 * @throws InvalidArgumentException
	 */
	public function addOpenBlock($name, $callable) {
		$this->validateExtensionBlock($name, $callable, 'Open block');
		$this->openBlocks[$name] = $callable;
	}

	/**
	 * Ensures that the arguments to addOpenBlock and addClosedBlock are valid
	 * @param $name
	 * @param $callable
	 * @param $type
	 * @throws InvalidArgumentException
	 */
	protected function validateExtensionBlock($name, $callable, $type) {
		if (!is_string($name)) {
			throw new InvalidArgumentException(
				sprintf(
					"Name argument for %s must be a string",
					$type
				)
			);
		} elseif (!is_callable($callable)) {
			throw new InvalidArgumentException(
				sprintf(
					"Callable %s argument named '%s' is not callable",
					$type,
					$name
				)
			);
		}
	}
	
	/*!* SSTemplateParser

	# Template is any structurally-complete portion of template (a full nested level in other words). It's the 
	# primary matcher, and is used by all enclosing blocks, as well as a base for the top level.
	# Any new template elements need to be included in this list, if they are to work.
	 
	Template: (Comment | Translate | If | Require | CacheBlock | UncachedBlock | OldI18NTag | Include | ClosedBlock |
		OpenBlock | MalformedBlock | Injection | Text)+
	*/
	function Template_STR(&$res, $sub) {
		$res['php'] .= $sub['php'] . PHP_EOL ;
	}
	
	/*!*
	
	Word: / [A-Za-z_] [A-Za-z0-9_]* /
	Number: / [0-9]+ /
	Value: / [A-Za-z0-9_]+ /

	# CallArguments is a list of one or more comma seperated "arguments" (lookups or strings, either bare or marked)
	# as passed to a Call within brackets
	 
	CallArguments: :Argument ( < "," < :Argument )*
	*/

	/** 
	 * Values are bare words in templates, but strings in PHP. We rely on PHP's type conversion to back-convert
	 * strings to numbers when needed.
	 */
	function CallArguments_Argument(&$res, $sub) {
		if (!empty($res['php'])) $res['php'] .= ', ';
		
		$res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : 
			str_replace('$$FINAL', 'XML_val', $sub['php']);
	}

	/*!*
	
	# Call is a php-style function call, e.g. Method(Argument, ...). Unlike PHP, the brackets are optional if no
	# arguments are passed
	 
	Call: Method:Word ( "(" < :CallArguments? > ")" )?

	# A lookup is a lookup of a value on the current scope object. It's a sequence of calls seperated by "."
	# characters. This final call in the sequence needs handling specially, as different structures need different 
	# sorts of values, which require a different final method to be called to get the right return value
	 
	LookupStep: :Call &"."
	LastLookupStep: :Call

	Lookup: LookupStep ("." LookupStep)* "." LastLookupStep | LastLookupStep
	*/
	
	function Lookup__construct(&$res) {
		$res['php'] = '$scope->locally()';
		$res['LookupSteps'] = array();
	}
	
	/** 
	 * The basic generated PHP of LookupStep and LastLookupStep is the same, except that LookupStep calls 'obj' to 
	 * get the next ViewableData in the sequence, and LastLookupStep calls different methods (XML_val, hasValue, obj)
	 * depending on the context the lookup is used in.
	 */
	function Lookup_AddLookupStep(&$res, $sub, $method) {
		$res['LookupSteps'][] = $sub;
		
		$property = $sub['Call']['Method']['text'];
		
		if (isset($sub['Call']['CallArguments']) && $arguments = $sub['Call']['CallArguments']['php']) {
			$res['php'] .= "->$method('$property', array($arguments), true)";
		}
		else {
			$res['php'] .= "->$method('$property', null, true)";
		}
	}

	function Lookup_LookupStep(&$res, $sub) {
		$this->Lookup_AddLookupStep($res, $sub, 'obj');
	}

	function Lookup_LastLookupStep(&$res, $sub) {
		$this->Lookup_AddLookupStep($res, $sub, '$$FINAL');
	}


	/*!*

	# New Translatable Syntax
	# <%t Entity DefaultString is Context name1=string name2=$functionCall
	# (This is a new way to call translatable strings. The parser transforms this into a call to the _t() method)

	Translate: "<%t" < Entity < (Default:QuotedString)? < (!("is" "=") < "is" < Context:QuotedString)? <
		(InjectionVariables)? > "%>"
	InjectionVariables: (< InjectionName:Word "=" Argument)+
	Entity: / [A-Za-z_] [\w\.]* /
	*/

	function Translate__construct(&$res) {
		$res['php'] = '$val .= _t(';
	}

	function Translate_Entity(&$res, $sub) {
		$res['php'] .= "'$sub[text]'";
	}

	function Translate_Default(&$res, $sub) {
		$res['php'] .= ",$sub[text]";
	}

	function Translate_Context(&$res, $sub) {
		$res['php'] .= ",$sub[text]";
	}

	function Translate_InjectionVariables(&$res, $sub) {
		$res['php'] .= ",$sub[php]";
	}

	function Translate__finalise(&$res) {
		$res['php'] .= ');';
	}

	function InjectionVariables__construct(&$res) {
		$res['php'] = "array(";
	}

	function InjectionVariables_InjectionName(&$res, $sub) {
		$res['php'] .= "'$sub[text]'=>";
	}

	function InjectionVariables_Argument(&$res, $sub) {
		$res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']) . ',';
	}

	function InjectionVariables__finalise(&$res) {
		if (substr($res['php'], -1) == ',') $res['php'] = substr($res['php'], 0, -1); //remove last comma in the array
		$res['php'] .= ')';
	}


	/*!*
	
	# Injections are where, outside of a block, a value needs to be inserted into the output. You can either
	# just do $Foo, or {$Foo} if the surrounding text would cause a problem (e.g. {$Foo}Bar)
	 
	SimpleInjection: '$' :Lookup
	BracketInjection: '{$' :Lookup "}"
	Injection: BracketInjection | SimpleInjection
	*/
	function Injection_STR(&$res, $sub) {
		$res['php'] = '$val .= '. str_replace('$$FINAL', 'XML_val', $sub['Lookup']['php']) . ';';
	}

	/*!*
	 
	# Inside a block's arguments you can still use the same format as a simple injection ($Foo). In this case
	# it marks the argument as being a lookup, not a string (if it was bare it might still be used as a lookup,
	# but that depends on where it's used, a la 2.4)
	 
	DollarMarkedLookup: SimpleInjection
	*/
	function DollarMarkedLookup_STR(&$res, $sub) {
		$res['Lookup'] = $sub['Lookup'];
	}

	/*!*
	
	# Inside a block's arguments you can explictly mark a string by surrounding it with quotes (single or double,
	# but they must be matching). If you do, inside the quote you can escape any character, but the only character
	# that _needs_ escaping is the matching closing quote 
	 
	QuotedString: q:/['"]/   String:/ (\\\\ | \\. | [^$q\\])* /   '$q'
	
	# In order to support 2.4's base syntax, we also need to detect free strings - strings not surrounded by
	# quotes, and containing spaces or punctuation, but supported as a single string. We support almost as flexible
	# a string as 2.4 - we don't attempt to determine the closing character by context, but just break on any
	# character which, in some context, would indicate the end of a free string, regardless of if we're actually in
	# that context or not 
	 
	FreeString: /[^,)%!=><|&]+/
	
	# An argument - either a marked value, or a bare value, prefering lookup matching on the bare value over
	# freestring matching as long as that would give a successful parse 
	 
	Argument:
		:DollarMarkedLookup |
		:QuotedString |
		:Lookup !(< FreeString)|
		:FreeString
	*/
	
	/**
	 * If we get a bare value, we don't know enough to determine exactly what php would be the translation, because
	 * we don't know if the position of use indicates a lookup or a string argument.
	 * 
	 * Instead, we record 'ArgumentMode' as a member of this matches results node, which can be:
	 *   - lookup if this argument was unambiguously a lookup (marked as such)
	 *   - string is this argument was unambiguously a string (marked as such, or impossible to parse as lookup)
	 *   - default if this argument needs to be handled as per 2.4
	 * 
	 * In the case of 'default', there is no php member of the results node, but instead 'lookup_php', which
	 * should be used by the parent if the context indicates a lookup, and 'string_php' which should be used
	 * if the context indicates a string
	 */
	
	function Argument_DollarMarkedLookup(&$res, $sub) {
		$res['ArgumentMode'] = 'lookup';
		$res['php'] = $sub['Lookup']['php'];
	}

	function Argument_QuotedString(&$res, $sub) {
		$res['ArgumentMode'] = 'string';
		$res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'";
	}

	function Argument_Lookup(&$res, $sub) {
		if (count($sub['LookupSteps']) == 1 && !isset($sub['LookupSteps'][0]['Call']['Arguments'])) {
			$res['ArgumentMode'] = 'default';
			$res['lookup_php'] = $sub['php'];
			$res['string_php'] = "'".$sub['LookupSteps'][0]['Call']['Method']['text']."'";
		}
		else {
			$res['ArgumentMode'] = 'lookup';
			$res['php'] = $sub['php'];
		}
	}
	
	function Argument_FreeString(&$res, $sub) {
		$res['ArgumentMode'] = 'string';
		$res['php'] = "'" . str_replace("'", "\\'", trim($sub['text'])) . "'";
	}
	
	/*!*
	 
	# if and else_if blocks allow basic comparisons between arguments
	 
	ComparisonOperator: "!=" | "==" | ">=" | ">" | "<=" | "<" | "="

	Comparison: Argument < ComparisonOperator > Argument
	*/
	function Comparison_Argument(&$res, $sub) {
		if ($sub['ArgumentMode'] == 'default') {
			if (!empty($res['php'])) $res['php'] .= $sub['string_php'];
			else $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']);
		}	
		else {
			$res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']);
		}
	}

	function Comparison_ComparisonOperator(&$res, $sub) {
		$res['php'] .= ($sub['text'] == '=' ? '==' : $sub['text']);
	}

	/*!*
	 
	# If a comparison operator is not used in an if or else_if block, then the statement is a 'presence check',
	# which checks if the argument given is present or not. For explicit strings (which were not allowed in 2.4)
	# this falls back to simple truthiness check
	 
	PresenceCheck: (Not:'not' <)? Argument
	*/
	function PresenceCheck_Not(&$res, $sub) {
		$res['php'] = '!';
	}
	
	function PresenceCheck_Argument(&$res, $sub) {
		if ($sub['ArgumentMode'] == 'string') {
			$res['php'] .= '((bool)'.$sub['php'].')';
		}
		else {
			$php = ($sub['ArgumentMode'] == 'default' ? $sub['lookup_php'] : $sub['php']);
			// TODO: kinda hacky - maybe we need a way to pass state down the parse chain so
			// Lookup_LastLookupStep and Argument_BareWord can produce hasValue instead of XML_val
			$res['php'] .= str_replace('$$FINAL', 'hasValue', $php);
		}
	}

	/*!*	
	 
	# if and else_if arguments are a series of presence checks and comparisons, optionally seperated by boolean 
	# operators 
	 
	IfArgumentPortion: Comparison | PresenceCheck
	*/
	function IfArgumentPortion_STR(&$res, $sub) {
		$res['php'] = $sub['php'];
	}

	/*!*
	 
	# if and else_if arguments can be combined via these two boolean operators. No precendence overriding is
	# supported
	 	
	BooleanOperator: "||" | "&&"
	
	# This is the combination of the previous if and else_if argument portions 
	 
	IfArgument: :IfArgumentPortion ( < :BooleanOperator < :IfArgumentPortion )*
	*/
	function IfArgument_IfArgumentPortion(&$res, $sub) {
		$res['php'] .= $sub['php'];
	}

	function IfArgument_BooleanOperator(&$res, $sub) {
		$res['php'] .= $sub['text'];
	}

	/*!*	
	 
	# ifs are handled seperately from other closed block tags, because (A) their structure is different - they
	# can have else_if and else tags in between the if tag and the end_if tag, and (B) they have a different
	# argument structure to every other block 
	 
	IfPart: '<%' < 'if' [ :IfArgument > '%>' Template:$TemplateMatcher?
	ElseIfPart: '<%' < 'else_if' [ :IfArgument > '%>' Template:$TemplateMatcher?
	ElsePart: '<%' < 'else' > '%>' Template:$TemplateMatcher?

	If: IfPart ElseIfPart* ElsePart? '<%' < 'end_if' > '%>'
	*/
	function If_IfPart(&$res, $sub) {
		$res['php'] = 
			'if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL .
				(isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
			'}';
	} 

	function If_ElseIfPart(&$res, $sub) {
		$res['php'] .= 
			'else if (' . $sub['IfArgument']['php'] . ') { ' . PHP_EOL .
				(isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
			'}';
	}

	function If_ElsePart(&$res, $sub) {
		$res['php'] .= 
			'else { ' . PHP_EOL . 
				(isset($sub['Template']) ? $sub['Template']['php'] : '') . PHP_EOL .
			'}';
	}

	/*!*
	
	# The require block is handled seperately to the other open blocks as the argument syntax is different
	# - must have one call style argument, must pass arguments to that call style argument
	 
	Require: '<%' < 'require' [ Call:(Method:Word "(" < :CallArguments  > ")") > '%>'
	*/
	function Require_Call(&$res, $sub) {
		$res['php'] = "Requirements::".$sub['Method']['text'].'('.$sub['CallArguments']['php'].');';
	}

	
	/*!*
	
	# Cache block arguments don't support free strings
	
	CacheBlockArgument:
	   !( "if " | "unless " )
		( 
			:DollarMarkedLookup |
			:QuotedString |
			:Lookup
		)
	*/
	function CacheBlockArgument_DollarMarkedLookup(&$res, $sub) {
		$res['php'] = $sub['Lookup']['php'];
	}
	
	function CacheBlockArgument_QuotedString(&$res, $sub) {
		$res['php'] = "'" . str_replace("'", "\\'", $sub['String']['text']) . "'";
	}
	
	function CacheBlockArgument_Lookup(&$res, $sub) {
		$res['php'] = $sub['php'];
	}
		
	/*!*
	
	# Collects the arguments passed in to be part of the key of a cacheblock
	 
	CacheBlockArguments: CacheBlockArgument ( < "," < CacheBlockArgument )*
	 
	*/
	function CacheBlockArguments_CacheBlockArgument(&$res, $sub) {
		if (!empty($res['php'])) $res['php'] .= ".'_'.";
		else $res['php'] = '';
		
		$res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']);
	}
	
	/*!*
	# CacheBlockTemplate is the same as Template, but doesn't include cache blocks (because they're handled seperately)
	 
	CacheBlockTemplate extends Template (TemplateMatcher = CacheRestrictedTemplate); CacheBlock | UncachedBlock | => ''
	*/
		
	/*!*
	  
	UncachedBlock: 
		'<%' < "uncached" < CacheBlockArguments? ( < Conditional:("if"|"unless") > Condition:IfArgument )? > '%>'
			Template:$TemplateMatcher?
			'<%' < 'end_' ("uncached"|"cached"|"cacheblock") > '%>'
	*/
	function UncachedBlock_Template(&$res, $sub){
		$res['php'] = $sub['php'];
	}
	
	/*!*
	
	# CacheRestrictedTemplate is the same as Template, but doesn't allow cache blocks
	 
	CacheRestrictedTemplate extends Template
	*/
	function CacheRestrictedTemplate_CacheBlock(&$res, $sub) { 
		throw new SSTemplateParseException('You cant have cache blocks nested within with, loop or control blocks ' .
			'that are within cache blocks', $this);
	}
	
	function CacheRestrictedTemplate_UncachedBlock(&$res, $sub) { 
		throw new SSTemplateParseException('You cant have uncache blocks nested within with, loop or control blocks ' .
			'that are within cache blocks', $this);
	}
	
	/*!*
	# The partial caching block
	 
	CacheBlock: 
		'<%' < CacheTag:("cached"|"cacheblock") < (CacheBlockArguments)? ( < Conditional:("if"|"unless") >
		Condition:IfArgument )? > '%>'
			(CacheBlock | UncachedBlock | CacheBlockTemplate)*
		'<%' < 'end_' ("cached"|"uncached"|"cacheblock") > '%>'
	 
	*/
	function CacheBlock__construct(&$res){
		$res['subblocks'] = 0;
	}
	
	function CacheBlock_CacheBlockArguments(&$res, $sub){
		$res['key'] = !empty($sub['php']) ? $sub['php'] : '';
	}
	
	function CacheBlock_Condition(&$res, $sub){
		$res['condition'] = ($res['Conditional']['text'] == 'if' ? '(' : '!(') . $sub['php'] . ') && ';
	}
	
	function CacheBlock_CacheBlock(&$res, $sub){
		$res['php'] .= $sub['php'];
	}
	
	function CacheBlock_UncachedBlock(&$res, $sub){
		$res['php'] .= $sub['php'];
	}
	
	function CacheBlock_CacheBlockTemplate(&$res, $sub){
		// Get the block counter
		$block = ++$res['subblocks'];
		// Build the key for this block from the global key (evaluated in a closure within the template),
		// the passed cache key, the block index, and the sha hash of the template.
		$res['php'] .= '$keyExpression = function() use ($scope, $cache) {' . PHP_EOL;
		$res['php'] .= '$val = \'\';' . PHP_EOL;
		if($globalKey = Config::inst()->get('SSViewer', 'global_key')) {
			// Embed the code necessary to evaluate the globalKey directly into the template,
			// so that SSTemplateParser only needs to be called during template regeneration.
			// Warning: If the global key is changed, it's necessary to flush the template cache.
			$parser = Injector::inst()->get('SSTemplateParser', false);
			$result = $parser->compileString($globalKey, '', false, false);
			if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $parser);
			$res['php'] .= $result . PHP_EOL;
		}
		$res['php'] .= 'return $val;' . PHP_EOL;
		$res['php'] .= '};' . PHP_EOL;
		$key = 'sha1($keyExpression())' // Global key
			. '.\'_' . sha1($sub['php']) // sha of template
			. (isset($res['key']) && $res['key'] ? "_'.sha1(".$res['key'].")" : "'") // Passed key
			. ".'_$block'"; // block index
		// Get any condition
		$condition = isset($res['condition']) ? $res['condition'] : '';
		
		$res['php'] .= 'if ('.$condition.'($partial = $cache->load('.$key.'))) $val .= $partial;' . PHP_EOL;
		$res['php'] .= 'else { $oldval = $val; $val = "";' . PHP_EOL;
		$res['php'] .= $sub['php'] . PHP_EOL;
		$res['php'] .= $condition . ' $cache->save($val); $val = $oldval . $val;' . PHP_EOL;
		$res['php'] .= '}';
	}
	
	/*!*
	 
	# Deprecated old-style i18n _t and sprintf(_t block tags. We support a slightly more flexible version than we used
	# to, but just because it's easier to do so. It's strongly recommended to use the new syntax
	 
	# This is the core used by both syntaxes, without the block start & end tags
	 
	OldTPart: "_t" N "(" N QuotedString (N "," N CallArguments)? N ")" N (";")?

	# whitespace with a newline
	N: / [\s\n]* /
	*/
	function OldTPart__construct(&$res) {
		$res['php'] = "_t(";
	}
	
	function OldTPart_QuotedString(&$res, $sub) {
		$entity = $sub['String']['text'];
		if (strpos($entity, '.') === false) {
			$res['php'] .= "\$scope->XML_val('I18NNamespace').'.$entity'";
		}
		else {
			$res['php'] .= "'$entity'";
		}
	}
	
	function OldTPart_CallArguments(&$res, $sub) {
		$res['php'] .= ',' . $sub['php'];
	}

	function OldTPart__finalise(&$res) {
		$res['php'] .= ')';
	}
	
	/*!*
	 
	# This is the old <% _t() %> tag
	 
	OldTTag: "<%" < OldTPart > "%>"
	
	*/
	function OldTTag_OldTPart(&$res, $sub) {
		$res['php'] = $sub['php'];
	}

	/*!*
	 
	# This is the old <% sprintf(_t()) %> tag
	 
	OldSprintfTag: "<%" < "sprintf" < "(" < OldTPart < "," < CallArguments > ")" > "%>" 
	
	*/
	function OldSprintfTag__construct(&$res) {
		$res['php'] = "sprintf(";
	}
	
	function OldSprintfTag_OldTPart(&$res, $sub) {
		$res['php'] .= $sub['php'];
	}

	function OldSprintfTag_CallArguments(&$res, $sub) {
		$res['php'] .= ',' . $sub['php'] . ')';
	}
	
	/*!*
	
	# This matches either the old style sprintf(_t()) or _t() tags. As well as including the output portion of the
	# php, this rule combines all the old i18n stuff into a single match rule to make it easy to not support these
	# tags later 
	 
	OldI18NTag: OldSprintfTag | OldTTag
	 
	*/
	function OldI18NTag_STR(&$res, $sub) {
		$res['php'] = '$val .= ' . $sub['php'] . ';';
	}

	/*!*

	# An argument that can be passed through to an included template

	NamedArgument: Name:Word "=" Value:Argument

	*/
	function NamedArgument_Name(&$res, $sub) {
		$res['php'] = "'" . $sub['text'] . "' => ";
	}

	function NamedArgument_Value(&$res, $sub) {
		switch($sub['ArgumentMode']) {
			case 'string':
				$res['php'] .= $sub['php'];
				break;

			case 'default':
				$res['php'] .= $sub['string_php'];
				break;

			default:
				$res['php'] .= str_replace('$$FINAL', 'obj', $sub['php']) . '->self()';
				break;
		}
	}

	/*!*

	# The include tag

	Include: "<%" < "include" < Template:Word < (NamedArgument ( < "," < NamedArgument )*)? > "%>"

	*/
	function Include__construct(&$res){
		$res['arguments'] = array();
	}

	function Include_Template(&$res, $sub){
		$res['template'] = "'" . $sub['text'] . "'";
	}

	function Include_NamedArgument(&$res, $sub){
		$res['arguments'][] = $sub['php'];
	}

	function Include__finalise(&$res){
		$template = $res['template'];
		$arguments = $res['arguments'];

		$res['php'] = '$val .= SSViewer::execute_template('.$template.', $scope->getItem(), array(' . 
			implode(',', $arguments)."), \$scope);\n";

		if($this->includeDebuggingComments) { // Add include filename comments on dev sites
			$res['php'] =
				'$val .= \'<!-- include '.addslashes($template).' -->\';'. "\n".
				$res['php'].
				'$val .= \'<!-- end include '.addslashes($template).' -->\';'. "\n";
		}
	}

	/*!*
	
	# To make the block support reasonably extendable, we don't explicitly define each closed block and it's structure,
	# but instead match against a generic <% block_name argument, ... %> pattern. Each argument is left as per the
	# output of the Argument matcher, and the handler (see the PHPDoc block later for more on this) is responsible
	# for pulling out the info required 
	 
	BlockArguments: :Argument ( < "," < :Argument)* 
	
	# NotBlockTag matches against any word that might come after a "<%" that the generic open and closed block handlers
	# shouldn't attempt to match against, because they're handled by more explicit matchers 
	 
	NotBlockTag: "end_" | (("if" | "else_if" | "else" | "require" | "cached" | "uncached" | "cacheblock" | "include")])
	
	# Match against closed blocks - blocks with an opening and a closing tag that surround some internal portion of
	# template
	 
	ClosedBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > Zap:'%>' Template:$TemplateMatcher? 
		'<%' < 'end_' '$BlockName' > '%>'
	*/
	
	/**
	 * As mentioned in the parser comment, block handling is kept fairly generic for extensibility. The match rule
	 * builds up two important elements in the match result array:
	 *   'ArgumentCount' - how many arguments were passed in the opening tag
	 *   'Arguments' an array of the Argument match rule result arrays
	 *
	 * Once a block has successfully been matched against, it will then look for the actual handler, which should
	 * be on this class (either defined or extended on) as ClosedBlock_Handler_Name(&$res), where Name is the
	 * tag name, first letter captialized (i.e Control, Loop, With, etc).
	 * 
	 * This function will be called with the match rule result array as it's first argument. It should return
	 * the php result of this block as it's return value, or throw an error if incorrect arguments were passed.
	 */
	
	function ClosedBlock__construct(&$res) {
		$res['ArgumentCount'] = 0;
	}
	
	function ClosedBlock_BlockArguments(&$res, $sub) {
		if (isset($sub['Argument']['ArgumentMode'])) {
			$res['Arguments'] = array($sub['Argument']);
			$res['ArgumentCount'] = 1;
		}
		else {
			$res['Arguments'] = $sub['Argument'];
			$res['ArgumentCount'] = count($res['Arguments']);
		}
	}

	function ClosedBlock__finalise(&$res) {
		$blockname = $res['BlockName']['text'];

		$method = 'ClosedBlock_Handle_'.$blockname;
		if (method_exists($this, $method)) {
			$res['php'] = $this->$method($res);
		} else if (isset($this->closedBlocks[$blockname])) {
			$res['php'] = call_user_func($this->closedBlocks[$blockname], $res);
		} else {
			throw new SSTemplateParseException('Unknown closed block "'.$blockname.'" encountered. Perhaps you are ' .
			'not supposed to close this block, or have mis-spelled it?', $this);
		}
	}

	/**
	 * This is an example of a block handler function. This one handles the loop tag.
	 */
	function ClosedBlock_Handle_Loop(&$res) {
		if ($res['ArgumentCount'] > 1) {
			throw new SSTemplateParseException('Either no or too many arguments in control block. Must be one ' .
				'argument only.', $this);
		}

		//loop without arguments loops on the current scope
		if ($res['ArgumentCount'] == 0) {
			$on = '$scope->obj(\'Up\', null, true)->obj(\'Foo\', null, true)';
		} else {    //loop in the normal way
			$arg = $res['Arguments'][0];
			if ($arg['ArgumentMode'] == 'string') {
				throw new SSTemplateParseException('Control block cant take string as argument.', $this);
			}
			$on = str_replace('$$FINAL', 'obj', 
				($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']);
		}

		return
			$on . '; $scope->pushScope(); while (($key = $scope->next()) !== false) {' . PHP_EOL .
				$res['Template']['php'] . PHP_EOL .
			'}; $scope->popScope(); ';
	}

	/**
	 * The deprecated closed block handler for control blocks
	 * @deprecated
	 */
	function ClosedBlock_Handle_Control(&$res) {
		Deprecation::notice('3.1', '<% control %> is deprecated. Use <% with %> or <% loop %> instead.');
		return $this->ClosedBlock_Handle_Loop($res);
	}
	
	/**
	 * The closed block handler for with blocks
	 */
	function ClosedBlock_Handle_With(&$res) {
		if ($res['ArgumentCount'] != 1) {
			throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one ' .
				'argument only.', $this);
		}
		
		$arg = $res['Arguments'][0];
		if ($arg['ArgumentMode'] == 'string') {
			throw new SSTemplateParseException('Control block cant take string as argument.', $this);
		}
		
		$on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']);
		return 
			$on . '; $scope->pushScope();' . PHP_EOL .
				$res['Template']['php'] . PHP_EOL .
			'; $scope->popScope(); ';
	}
	
	/*!*
	 
	# Open blocks are handled in the same generic manner as closed blocks. There is no need to define which blocks
	# are which - closed is tried first, and if no matching end tag is found, open is tried next
	  
	OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>'
	*/
	function OpenBlock__construct(&$res) {
		$res['ArgumentCount'] = 0;
	}
	
	function OpenBlock_BlockArguments(&$res, $sub) {
		if (isset($sub['Argument']['ArgumentMode'])) {
			$res['Arguments'] = array($sub['Argument']);
			$res['ArgumentCount'] = 1;
		}
		else {
			$res['Arguments'] = $sub['Argument'];
			$res['ArgumentCount'] = count($res['Arguments']);
		}
	}

	function OpenBlock__finalise(&$res) {
		$blockname = $res['BlockName']['text'];

		$method = 'OpenBlock_Handle_'.$blockname;
		if (method_exists($this, $method)) {
			$res['php'] = $this->$method($res);
		} elseif (isset($this->openBlocks[$blockname])) {
			$res['php'] = call_user_func($this->openBlocks[$blockname], $res);
		} else {
			throw new SSTemplateParseException('Unknown open block "'.$blockname.'" encountered. Perhaps you missed ' .
			' the closing tag or have mis-spelled it?', $this);
		}
	}

	/**
	 * This is an open block handler, for the <% debug %> utility tag
	 */
	function OpenBlock_Handle_Debug(&$res) {
		if ($res['ArgumentCount'] == 0) return '$scope->debug();';
		else if ($res['ArgumentCount'] == 1) {
			$arg = $res['Arguments'][0];
			
			if ($arg['ArgumentMode'] == 'string') return 'Debug::show('.$arg['php'].');';
			
			$php = ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php'];
			return '$val .= Debug::show('.str_replace('FINALGET!', 'cachedCall', $php).');';
		}
		else {
			throw new SSTemplateParseException('Debug takes 0 or 1 argument only.', $this);
		}
	}

	/**
	 * This is an open block handler, for the <% base_tag %> tag
	 */
	function OpenBlock_Handle_Base_tag(&$res) {
		if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Base_tag takes no arguments', $this);
		return '$val .= SSViewer::get_base_tag($val);';
	}

	/**
	 * This is an open block handler, for the <% current_page %> tag
	 */
	function OpenBlock_Handle_Current_page(&$res) {
		if ($res['ArgumentCount'] != 0) throw new SSTemplateParseException('Current_page takes no arguments', $this);
		return '$val .= $_SERVER[SCRIPT_URL];';
	}
	
	/*!*
	
	# This is used to detect when we have a mismatched closing tag (i.e., one with no equivilent opening tag)
	# Because of parser limitations, this can only be used at the top nesting level of a template. Other mismatched
	# closing tags are detected as an invalid open tag  
	 
	MismatchedEndBlock: '<%' < 'end_' :Word > '%>'
	*/
	function MismatchedEndBlock__finalise(&$res) {
		$blockname = $res['Word']['text'];
		throw new SSTemplateParseException('Unexpected close tag end_' . $blockname . 
			' encountered. Perhaps you have mis-nested blocks, or have mis-spelled a tag?', $this);
	}

	/*!*	
	 
	# This is used to detect a malformed opening tag - one where the tag is opened with the "<%" characters, but
	# the tag is not structured properly 
	 
	MalformedOpenTag: '<%' < !NotBlockTag Tag:Word  !( ( [ :BlockArguments ] )? > '%>' )
	*/
	function MalformedOpenTag__finalise(&$res) {
		$tag = $res['Tag']['text'];
		throw new SSTemplateParseException("Malformed opening block tag $tag. Perhaps you have tried to use operators?"
			, $this);
	}
	
	/*!*
	 
	# This is used to detect a malformed end tag - one where the tag is opened with the "<%" characters, but
	# the tag is not structured properly 
	 
	MalformedCloseTag: '<%' < Tag:('end_' :Word ) !( > '%>' )
	*/
	function MalformedCloseTag__finalise(&$res) {
		$tag = $res['Tag']['text'];
		throw new SSTemplateParseException("Malformed closing block tag $tag. Perhaps you have tried to pass an " .
			"argument to one?", $this);
	}
	
	/*!*
	 
	# This is used to detect a malformed tag. It's mostly to keep the Template match rule a bit shorter
	 
	MalformedBlock: MalformedOpenTag | MalformedCloseTag
	*/

	/*!*
	 
	# This is used to remove template comments
	 
	Comment: "<%--" (!"--%>" /(?s)./)+ "--%>"
	*/
	function Comment__construct(&$res) {
		$res['php'] = '';
	}
		
	/*!*
	
	# TopTemplate is the same as Template, but should only be used at the top level (not nested), as it includes
	# MismatchedEndBlock detection, which only works at the top level  
	 
	TopTemplate extends Template (TemplateMatcher = Template); MalformedBlock => MalformedBlock | MismatchedEndBlock
	*/
	
	/**
	 * The TopTemplate also includes the opening stanza to start off the template
	 */
	function TopTemplate__construct(&$res) {
		$res['php'] = "<?php" . PHP_EOL;
	}

	/*!*
	
	# Text matches anything that isn't a template command (not an injection, block of any kind or comment) 
	 
	Text: (
			# Any set of characters that aren't potentially a control mark or an escaped character 
			/ [^<${\\]+ / |
			# An escaped character
			/ (\\.) / |
			# A '<' that isn't the start of a block tag
			'<' !'%' |
			# A '$' that isn't the start of an injection
			'$' !(/[A-Za-z_]/) |
			# A '{' that isn't the start of an injection
			'{' !'$' |
			# A '{$' that isn't the start of an injection
			'{$' !(/[A-Za-z_]/)
		)+
	*/
	
	/**
	 * We convert text 
	 */
	function Text__finalise(&$res) {
		$text = $res['text'];
		
		// Unescape any escaped characters in the text, then put back escapes for any single quotes and backslashes
		$text = stripslashes($text);
		$text = addcslashes($text, '\'\\');

		// TODO: This is pretty ugly & gets applied on all files not just html. I wonder if we can make this
		// non-dynamically calculated
		$text = preg_replace(
			'/(<a[^>]+href *= *)"#/i',
			'\\1"\' . (Config::inst()->get(\'SSViewer\', \'rewrite_hash_links\') ?' .
			' Convert::raw2att( $_SERVER[\'REQUEST_URI\'] ) : "") .
				\'#',
			$text
		);

		$res['php'] .= '$val .= \'' . $text . '\';' . PHP_EOL;
	}
		
	/******************
	 * Here ends the parser itself. Below are utility methods to use the parser
	 */
	
	/**
	 * Compiles some passed template source code into the php code that will execute as per the template source.
	 * 
	 * @throws SSTemplateParseException
	 * @param  $string The source of the template
	 * @param string $templateName The name of the template, normally the filename the template source was loaded from
	 * @param bool $includeDebuggingComments True is debugging comments should be included in the output
	 * @param bool $topTemplate True if this is a top template, false if it's just a template
	 * @return mixed|string The php that, when executed (via include or exec) will behave as per the template source
	 */
	public function compileString($string, $templateName = "", $includeDebuggingComments=false, $topTemplate = true) {
		if (!trim($string)) {
			$code = '';
		}
		else {
			parent::__construct($string);
			
			$this->includeDebuggingComments = $includeDebuggingComments;
	
			// Ignore UTF8 BOM at begining of string. TODO: Confirm this is needed, make sure SSViewer handles UTF
			// (and other encodings) properly
			if(substr($string, 0,3) == pack("CCC", 0xef, 0xbb, 0xbf)) $this->pos = 3;
			
			// Match the source against the parser
			if ($topTemplate) {
				$result = $this->match_TopTemplate();
			} else {
				$result = $this->match_Template();
			}
			if(!$result) throw new SSTemplateParseException('Unexpected problem parsing template', $this);
	
			// Get the result
			$code = $result['php'];
		}

		// Include top level debugging comments if desired
		if($includeDebuggingComments && $templateName && stripos($code, "<?xml") === false) {
			$code = $this->includeDebuggingComments($code, $templateName);
		}	
		
		return $code;
	}

	/**
	 * @param string $code
	 * @return string $code
	 */
	protected function includeDebuggingComments($code, $templateName) {
		// If this template contains a doctype, put it right after it,
		// if not, put it after the <html> tag to avoid IE glitches
		if(stripos($code, "<!doctype") !== false) {
			$code = preg_replace('/(<!doctype[^>]*("[^"]")*[^>]*>)/im', "$1\r\n<!-- template $templateName -->", $code);
			$code .= "\r\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
		} elseif(stripos($code, "<html") !== false) {
			$code = preg_replace_callback('/(.*)(<html[^>]*>)(.*)/i', function($matches) use ($templateName) {
				if (stripos($matches[3], '<!--') === false && stripos($matches[3], '-->') !== false) {
					// after this <html> tag there is a comment close but no comment has been opened
					// this most likely means that this <html> tag is inside a comment
					// we should not add a comment inside a comment (invalid html)
					// lets append it at the end of the comment
					// an example case for this is the html5boilerplate: <!--[if IE]><html class="ie"><![endif]-->
					return $matches[0];
				} else {
					// all other cases, add the comment and return it
					return "{$matches[1]}{$matches[2]}<!-- template $templateName -->{$matches[3]}";
				}
			}, $code);
			$code = preg_replace('/(<\/html[^>]*>)/i', "<!-- end template $templateName -->$1", $code);
		} else {
			$code = str_replace('<?php' . PHP_EOL, '<?php' . PHP_EOL . '$val .= \'<!-- template ' . $templateName .
				' -->\';' . "\r\n", $code);
			$code .= "\r\n" . '$val .= \'<!-- end template ' . $templateName . ' -->\';';
		}
		return $code;
	}
	
	/**
	 * Compiles some file that contains template source code, and returns the php code that will execute as per that
	 * source
	 * 
	 * @static
	 * @param  $template - A file path that contains template source code
	 * @return mixed|string - The php that, when executed (via include or exec) will behave as per the template source
	 */
	public function compileFile($template) {
		return $this->compileString(file_get_contents($template), $template);
	}
}