2011-02-09 05:31:16 +01:00
< ? 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
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 THIRDPARTY_PATH . '/php-peg/Parser.php' ;
}
else {
$base = dirname ( __FILE__ );
require $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 .
*/
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
2011-02-14 22:03:42 +01:00
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
2011-02-09 05:31:16 +01:00
*/
class SSTemplateParser extends Parser {
2011-02-14 22:03:42 +01:00
/**
* @ var bool - Set true by SSTemplateParser :: compileString if the template should include comments intended
* for debugging ( template source , included files , etc )
*/
2011-02-09 05:31:16 +01:00
protected $includeDebuggingComments = false ;
function construct ( $name ) {
$result = parent :: construct ( $name );
$result [ 'tags' ] = array ();
return $result ;
}
/*!* SSTemplateParser
Word : / [ A - Za - z_ ] [ A - Za - z0 - 9_ ] * /
Number : / [ 0 - 9 ] + /
Value : / [ A - Za - z0 - 9_ ] + /
2011-02-14 22:03:42 +01:00
# 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 ) *
*/
2011-02-09 05:31:16 +01:00
2011-02-14 22:03:42 +01:00
/**
* 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 ) {
2011-02-09 05:31:16 +01:00
if ( isset ( $res [ 'php' ])) $res [ 'php' ] .= ', ' ;
else $res [ 'php' ] = '' ;
$res [ 'php' ] .= ( $sub [ 'ArgumentMode' ] == 'default' ) ? $sub [ 'string_php' ] : $sub [ 'php' ];
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
LookupStep : : Call & " . "
LastLookupStep : : Call
Lookup : LookupStep ( " . " LookupStep ) * " . " LastLookupStep | LastLookupStep
*/
function Lookup__construct ( & $res ) {
$res [ 'php' ] = '$item' ;
$res [ 'LookupSteps' ] = array ();
}
2011-02-14 22:03:42 +01:00
/**
* 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 .
*/
2011-02-09 05:31:16 +01:00
function Lookup_AddLookupStep ( & $res , $sub , $method ) {
$res [ 'LookupSteps' ][] = $sub ;
$property = $sub [ 'Call' ][ 'Method' ][ 'text' ];
2011-02-14 22:03:42 +01:00
if ( isset ( $sub [ 'Call' ][ 'CallArguments' ]) && $arguments = $sub [ 'Call' ][ 'CallArguments' ][ 'php' ]) {
2011-02-09 05:31:16 +01:00
$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 , 'XML_val' );
}
/*!*
2011-02-14 22:03:42 +01:00
# 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)
2011-02-09 05:31:16 +01:00
SimpleInjection : '$' : Lookup
BracketInjection : '{$' : Lookup " } "
Injection : BracketInjection | SimpleInjection
*/
function Injection_STR ( & $res , $sub ) {
$res [ 'php' ] = '$val .= ' . $sub [ 'Lookup' ][ 'php' ] . ';' ;
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
*/
function DollarMarkedLookup_STR ( & $res , $sub ) {
$res [ 'Lookup' ] = $sub [ 'Lookup' ];
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
QuotedString : q :/ [ '"]/ String:/ (\\\\ | \\. | [^$q\\])* / ' $q '
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
FreeString : / [ ^ ,) %!=|& ] +/
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
Argument :
: DollarMarkedLookup |
: QuotedString |
: Lookup ! ( < FreeString ) |
: FreeString
*/
2011-02-14 22:03:42 +01:00
/**
* 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
*/
2011-02-09 05:31:16 +01:00
function Argument_DollarMarkedLookup ( & $res , $sub ) {
$res [ 'ArgumentMode' ] = 'lookup' ;
$res [ 'php' ] = $sub [ 'Lookup' ][ 'php' ];
}
function Argument_QuotedString ( & $res , $sub ) {
$res [ 'ArgumentMode' ] = 'string' ;
$res [ 'php' ] = " ' " . $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' ] = " ' " . $sub [ 'text' ] . " ' " ;
}
/*!*
2011-02-14 22:03:42 +01:00
# if and else_if blocks allow basic comparisons between arguments
2011-02-09 05:31:16 +01:00
ComparisonOperator : " == " | " != " | " = "
Comparison : Argument < ComparisonOperator > Argument
*/
function Comparison_Argument ( & $res , $sub ) {
if ( $sub [ 'ArgumentMode' ] == 'default' ) {
if ( isset ( $res [ 'php' ])) $res [ 'php' ] .= $sub [ 'string_php' ];
else $res [ 'php' ] = $sub [ 'lookup_php' ];
}
else {
if ( ! isset ( $res [ 'php' ])) $res [ 'php' ] = '' ;
$res [ 'php' ] .= $sub [ 'php' ];
}
}
function Comparison_ComparisonOperator ( & $res , $sub ) {
$res [ 'php' ] .= ( $sub [ 'text' ] == '=' ? '==' : $sub [ 'text' ]);
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
PresenceCheck : Argument
*/
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 ( '->XML_val' , '->hasValue' , $php );
}
}
/*!*
2011-02-14 22:03:42 +01:00
# if and else_if arguments are a series of presence checks and comparisons, optionally seperated by boolean
# operators
2011-02-09 05:31:16 +01:00
IfArgumentPortion : Comparison | PresenceCheck
*/
function IfArgumentPortion_STR ( & $res , $sub ) {
$res [ 'php' ] = $sub [ 'php' ];
}
2011-02-14 22:03:42 +01:00
/*!*
# if and else_if arguments can be combined via these two boolean operators. No precendence overriding is supported
2011-02-09 05:31:16 +01:00
BooleanOperator : " || " | " && "
2011-02-14 22:03:42 +01:00
# This is the combination of the previous if and else_if argument portions
2011-02-09 05:31:16 +01:00
IfArgument : : IfArgumentPortion ( < : BooleanOperator < : IfArgumentPortion ) *
*/
function IfArgument__construct ( & $res ){
$res [ 'php' ] = '' ;
}
function IfArgument_IfArgumentPortion ( & $res , $sub ) {
$res [ 'php' ] .= $sub [ 'php' ];
}
function IfArgument_BooleanOperator ( & $res , $sub ) {
$res [ 'php' ] .= $sub [ 'text' ];
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
IfPart : '<%' < 'if' < : IfArgument > '%>' : Template ?
ElseIfPart : '<%' < 'else_if' < : IfArgument > '%>' : Template ?
ElsePart : '<%' < 'else' > '%>' : Template ?
If : IfPart ElseIfPart * ElsePart ? '<%' < 'end_if' > '%>'
*/
function If_IfPart ( & $res , $sub ) {
$res [ 'php' ] =
'if (' . $sub [ 'IfArgument' ][ 'php' ] . ') { ' . PHP_EOL .
$sub [ 'Template' ][ 'php' ] . PHP_EOL .
'}' ;
}
function If_ElseIfPart ( & $res , $sub ) {
$res [ 'php' ] .=
'else if (' . $sub [ 'IfArgument' ][ 'php' ] . ') { ' . PHP_EOL .
$sub [ 'Template' ][ 'php' ] . PHP_EOL .
'}' ;
}
function If_ElsePart ( & $res , $sub ) {
$res [ 'php' ] .=
'else { ' . PHP_EOL .
$sub [ 'Template' ][ 'php' ] . PHP_EOL .
'}' ;
}
/*!*
2011-02-14 22:03:42 +01:00
# 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 > " ) " ) > '%>'
2011-02-09 05:31:16 +01:00
*/
function Require_Call ( & $res , $sub ) {
2011-02-14 22:03:42 +01:00
$res [ 'php' ] = " Requirements:: " . $sub [ 'Method' ][ 'text' ] . '(' . $sub [ 'CallArguments' ][ 'php' ] . ');' ;
2011-02-09 05:31:16 +01:00
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
BlockArguments : : Argument ( < " , " < : Argument ) *
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
NotBlockTag : " end_ " | (( " if " | " else_if " | " else " | " require " ) ] )
2011-02-14 22:03:42 +01:00
# Match against closed blocks - blocks with an opening and a closing tag that surround some internal portion of
# template
2011-02-09 05:31:16 +01:00
ClosedBlock : '<%' < ! NotBlockTag BlockName : Word ( [ : BlockArguments ] ) ? > Zap : '%>' : Template ? '<%' < 'end_' '$BlockName' > '%>'
*/
2011-02-14 22:03:42 +01:00
/**
* 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 decorated 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 .
*/
2011-02-09 05:31:16 +01:00
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_' . ucfirst ( strtolower ( $blockname ));
if ( method_exists ( $this , $method )) $res [ 'php' ] = $this -> $method ( $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 );
}
}
2011-02-14 22:03:42 +01:00
/**
* This is an example of a block handler function . This one handles the control tag .
*/
2011-02-09 05:31:16 +01:00
function ClosedBlock_Handle_Control ( & $res ) {
if ( $res [ 'ArgumentCount' ] != 1 ) {
throw new SSTemplateParseException ( 'Either no or too many arguments in control 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 ( '->XML_val' , '->obj' , ( $arg [ 'ArgumentMode' ] == 'default' ) ? $arg [ 'lookup_php' ] : $arg [ 'php' ]);
return
'array_push($itemStack, $item); if($loop = ' . $on . ') foreach($loop as $key => $item) {' . PHP_EOL .
$res [ 'Template' ][ 'php' ] . PHP_EOL .
'} $item = array_pop($itemStack); ' ;
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-14 04:52:01 +01:00
OpenBlock : '<%' < ! NotBlockTag BlockName : Word ( [ : BlockArguments ] ) ? > '%>'
2011-02-09 05:31:16 +01:00
*/
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 ) {
2011-02-14 04:52:01 +01:00
$blockname = $res [ 'BlockName' ][ 'text' ];
2011-02-09 05:31:16 +01:00
$method = 'OpenBlock_Handle_' . ucfirst ( strtolower ( $blockname ));
if ( method_exists ( $this , $method )) $res [ 'php' ] = $this -> $method ( $res );
else {
throw new SSTemplateParseException ( 'Unknown open block "' . $blockname . '" encountered. Perhaps you missed the closing tag or have mis-spelled it?' , $this );
}
}
2011-02-14 22:03:42 +01:00
/**
* This is an open block handler , for the <% include %> tag
*/
2011-02-09 05:31:16 +01:00
function OpenBlock_Handle_Include ( & $res ) {
if ( $res [ 'ArgumentCount' ] != 1 ) throw new SSTemplateParseException ( 'Include takes exactly one argument' , $this );
$arg = $res [ 'Arguments' ][ 0 ];
$php = ( $arg [ 'ArgumentMode' ] == 'default' ) ? $arg [ 'string_php' ] : $arg [ 'php' ];
if ( $this -> includeDebuggingComments ) { // Add include filename comments on dev sites
return
'$val .= \'<!-- include ' . $php . ' -->\';' . " \n " .
'$val .= SSViewer::parse_template(' . $php . ', $item);' . " \n " .
'$val .= \'<!-- end include ' . $php . ' -->\';' . " \n " ;
}
else {
return
'$val .= SSViewer::execute_template(' . $php . ', $item);' . " \n " ;
}
}
2011-02-14 22:03:42 +01:00
/**
* This is an open block handler , for the <% debug %> utility tag
*/
2011-02-09 05:31:16 +01:00
function OpenBlock_Handle_Debug ( & $res ) {
if ( $res [ 'ArgumentCount' ] == 0 ) return 'Debug::show($item);' ;
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 );
}
}
2011-02-14 22:03:42 +01:00
/**
* This is an open block handler , for the <% base_tag %> tag
*/
2011-02-09 05:31:16 +01:00
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);' ;
}
2011-02-14 22:03:42 +01:00
/**
* This is an open block handler , for the <% current_page %> tag
*/
2011-02-09 05:31:16 +01:00
function OpenBlock_Handle_Current_page ( & $res ) {
if ( $res [ 'ArgumentCount' ] != 0 ) throw new SSTemplateParseException ( 'Current_page takes no arguments' , $this );
return '$val .= $_SERVER[SCRIPT_URL];' ;
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-14 04:52:01 +01:00
MismatchedEndBlock : '<%' < 'end_' Word > '%>'
2011-02-09 05:31:16 +01:00
*/
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 );
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
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 );
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
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 );
}
/*!*
2011-02-14 22:03:42 +01:00
# This is used to detect a malformed tag. It's mostly to keep the Template match rule a bit shorter
2011-02-09 05:31:16 +01:00
MalformedBlock : MalformedOpenTag | MalformedCloseTag
*/
/*!*
2011-02-14 22:03:42 +01:00
# This is used to remove template comments
2011-02-09 05:31:16 +01:00
Comment : " <%-- " ( ! " --%> " /./ ) + " --%> "
*/
function Comment__construct ( & $res ) {
$res [ 'php' ] = '' ;
}
/*!*
2011-02-14 22:03:42 +01:00
# Text matches anything that isn't a template command (not an injection, block of any kind or comment)
2011-02-09 05:31:16 +01:00
Text : /
(
( \\ . ) | # Any escaped character
([ ^< $ {]) | # Any character that isn't <, $ or {
( < [ ^% ]) | # < if not followed by %
( $ [ ^ A - Za - z_ ]) | # $ if not followed by A-Z, a-z or _
({[ ^ $ ]) | # { if not followed by $
({ $ [ ^ A - Za - z_ ]) # {$ if not followed A-Z, a-z or _
) +
/
2011-02-14 22:03:42 +01:00
# Template is any structurally-complete portion of template (a full nested level in other words). It's used
# by the block rules to recurse
2011-02-09 05:31:16 +01:00
Template : ( Comment | If | Require | ClosedBlock | OpenBlock | MalformedBlock | Injection | Text ) +
*/
function Template__construct ( & $res ) {
$res [ 'php' ] = '' ;
}
function Template_Text ( & $res , $sub ) {
$text = $sub [ 'text' ];
$text = preg_replace (
'/href\s*\=\s*\"\#/' ,
'href="<?= SSViewer::{dlr}options[\'rewriteHashlinks\'] ? Convert::raw2att( {dlr}_SERVER[\'REQUEST_URI\'] ) : "" ?>#' ,
$text
);
// TODO: using heredocs means any left over $ symbols will trigger PHP lookups, as will any escapes
// Will it break backwards compatibility to use ' quoted strings, and escape just the ' characters?
$res [ 'php' ] .=
'$val .= <<<SSVIEWER' . PHP_EOL .
$text . PHP_EOL .
'SSVIEWER;' . PHP_EOL ;
}
function Template_STR ( & $res , $sub ) {
$res [ 'php' ] .= $sub [ 'php' ] . PHP_EOL ;
}
/*!*
2011-02-14 22:03:42 +01:00
# 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
2011-02-09 05:31:16 +01:00
TopTemplate : ( Comment | If | Require | ClosedBlock | OpenBlock | MalformedBlock | MismatchedEndBlock | Injection | Text ) +
*/
2011-02-14 22:03:42 +01:00
/**
* The TopTemplate also includes the opening stanza to start off the template
*/
2011-02-09 05:31:16 +01:00
function TopTemplate__construct ( & $res ) {
$res [ 'php' ] = " <?php " . PHP_EOL ;
}
2011-02-14 22:03:42 +01:00
/**
* But otherwise handles producing the php the same as every other template block
*/
2011-02-09 05:31:16 +01:00
function TopTemplate_Text ( & $res , $sub ) { return $this -> Template_Text ( $res , $sub ); }
function TopTemplate_STR ( & $res , $sub ) { return $this -> Template_STR ( $res , $sub ); }
2011-02-14 22:03:42 +01:00
/******************
* 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 .
*
* @ static
* @ 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
* @ return mixed | string - The php that , when executed ( via include or exec ) will behave as per the template source
*/
2011-02-09 05:31:16 +01:00
static function compileString ( $string , $templateName = " " , $includeDebuggingComments = false ) {
2011-02-14 22:03:42 +01:00
// Construct a parser instance
2011-02-09 05:31:16 +01:00
$parser = new SSTemplateParser ( $string );
$parser -> 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 )) $parser -> pos = 3 ;
2011-02-14 22:03:42 +01:00
// Match the source against the parser
2011-02-09 05:31:16 +01:00
$result = $parser -> match_TopTemplate ();
if ( ! $result ) throw new SSTemplateParseException ( 'Unexpected problem parsing template' , $parser );
2011-02-14 22:03:42 +01:00
// Get the result
2011-02-09 05:31:16 +01:00
$code = $result [ 'php' ];
2011-02-14 22:03:42 +01:00
// Include top level debugging comments if desired
2011-02-09 05:31:16 +01:00
if ( $includeDebuggingComments && $templateName && stripos ( $code , " <?xml " ) === false ) {
// If this template is a full HTML page, then put the comments just inside the HTML tag to prevent any IE glitches
if ( stripos ( $code , " <html " ) !== false ) {
$code = preg_replace ( '/(<html[^>]*>)/i' , " \\ 1<!-- template $templateName --> " , $code );
$code = preg_replace ( '/(<\/html[^>]*>)/i' , " <!-- end template $templateName --> \\ 1 " , $code );
} else {
$code = " <!-- template $templateName --> \n " . $code . " \n <!-- end template $templateName --> " ;
}
}
return $code ;
}
2011-02-14 22:03:42 +01:00
/**
* 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
*/
2011-02-09 05:31:16 +01:00
static function compileFile ( $template ) {
2011-02-14 22:03:42 +01:00
return self :: compileString ( file_get_contents ( $template ), $template );
2011-02-09 05:31:16 +01:00
}
}