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
*/
class SSTemplateParser extends Parser {
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_ ] + /
Arguments : : 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 Arguments_Argument ( & $res , $sub ) {
if ( isset ( $res [ 'php' ])) $res [ 'php' ] .= ', ' ;
else $res [ 'php' ] = '' ;
$res [ 'php' ] .= ( $sub [ 'ArgumentMode' ] == 'default' ) ? $sub [ 'string_php' ] : $sub [ 'php' ];
}
/*!*
Call : Method : Word ( " ( " < : Arguments ? > " ) " ) ?
LookupStep : : Call & " . "
LastLookupStep : : Call
Lookup : LookupStep ( " . " LookupStep ) * " . " LastLookupStep | LastLookupStep
*/
function Lookup__construct ( & $res ) {
$res [ 'php' ] = '$item' ;
$res [ 'LookupSteps' ] = array ();
}
function Lookup_AddLookupStep ( & $res , $sub , $method ) {
$res [ 'LookupSteps' ][] = $sub ;
$property = $sub [ 'Call' ][ 'Method' ][ 'text' ];
if ( isset ( $sub [ 'Call' ][ 'Arguments' ]) && $arguments = $sub [ 'Call' ][ 'Arguments' ][ '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 , 'XML_val' );
}
/*!*
SimpleInjection : '$' : Lookup
BracketInjection : '{$' : Lookup " } "
Injection : BracketInjection | SimpleInjection
*/
function Injection_STR ( & $res , $sub ) {
$res [ 'php' ] = '$val .= ' . $sub [ 'Lookup' ][ 'php' ] . ';' ;
}
/*!*
DollarMarkedLookup : BracketInjection | SimpleInjection
*/
function DollarMarkedLookup_STR ( & $res , $sub ) {
$res [ 'Lookup' ] = $sub [ 'Lookup' ];
}
/*!*
QuotedString : q :/ [ '"]/ String:/ (\\\\ | \\. | [^$q\\])* / ' $q '
FreeString : / [ ^ ,) %!=|& ] +/
Argument :
: DollarMarkedLookup |
: QuotedString |
: Lookup ! ( < FreeString ) |
: FreeString
*/
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' ] . " ' " ;
}
/*!*
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' ]);
}
/*!*
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 );
}
}
/*!*
IfArgumentPortion : Comparison | PresenceCheck
*/
function IfArgumentPortion_STR ( & $res , $sub ) {
$res [ 'php' ] = $sub [ 'php' ];
}
/*!*
BooleanOperator : " || " | " && "
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' ];
}
/*!*
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 .
'}' ;
}
/*!*
Require : '<%' < 'require' [ Call : ( Method : Word " ( " < : Arguments > " ) " ) > '%>'
*/
function Require_Call ( & $res , $sub ) {
$res [ 'php' ] = " Requirements:: " . $sub [ 'Method' ][ 'text' ] . '(' . $sub [ 'Arguments' ][ 'php' ] . ');' ;
}
/*!*
BlockArguments : : Argument ( < " , " < : Argument ) *
NotBlockTag : " end_ " | (( " if " | " else_if " | " else " | " require " ) ] )
ClosedBlock : '<%' < ! NotBlockTag BlockName : Word ( [ : BlockArguments ] ) ? > Zap : '%>' : Template ? '<%' < 'end_' '$BlockName' > '%>'
*/
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 );
}
}
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 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 );
}
}
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 " ;
}
}
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 );
}
}
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);' ;
}
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 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 );
}
/*!*
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 );
}
/*!*
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 );
}
/*!*
MalformedBlock : MalformedOpenTag | MalformedCloseTag
*/
/*!*
Comment : " <%-- " ( ! " --%> " /./ ) + " --%> "
*/
function Comment__construct ( & $res ) {
$res [ 'php' ] = '' ;
}
/*!*
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 _
) +
/
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 ;
}
/*!*
TopTemplate : ( Comment | If | Require | ClosedBlock | OpenBlock | MalformedBlock | MismatchedEndBlock | Injection | Text ) +
*/
function TopTemplate__construct ( & $res ) {
$res [ 'php' ] = " <?php " . PHP_EOL ;
}
function TopTemplate_Text ( & $res , $sub ) { return $this -> Template_Text ( $res , $sub ); }
function TopTemplate_STR ( & $res , $sub ) { return $this -> Template_STR ( $res , $sub ); }
static function compileString ( $string , $templateName = " " , $includeDebuggingComments = false ) {
$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 ;
$result = $parser -> match_TopTemplate ();
if ( ! $result ) throw new SSTemplateParseException ( 'Unexpected problem parsing template' , $parser );
$code = $result [ 'php' ];
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 ;
}
static function compileFile ( $template ) {
return self :: compileString ( file_get_contents ( $template ));
}
}