diff --git a/_config/view.yml b/_config/view.yml
index fd8293c9f3d..c2ad469aa2b 100644
--- a/_config/view.yml
+++ b/_config/view.yml
@@ -3,4 +3,4 @@ Name: view-config
---
SilverStripe\Core\Injector\Injector:
SilverStripe\View\TemplateEngine:
- class: 'SilverStripe\View\SSTemplateEngine'
+ class: 'SilverStripe\TemplateEngine\SSTemplateEngine'
diff --git a/composer.json b/composer.json
index 0f2e499e177..430d4b009f5 100644
--- a/composer.json
+++ b/composer.json
@@ -40,6 +40,7 @@
"silverstripe/config": "^3",
"silverstripe/assets": "^3",
"silverstripe/supported-modules": "^1.1",
+ "silverstripe/template-engine": "^1",
"silverstripe/vendor-plugin": "^2",
"sminnee/callbacklist": "^0.1.1",
"symfony/cache": "^7.0",
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 723b740bbdb..ed73be69227 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -28,16 +28,9 @@
-
-
'; - foreach ($lines as $num => $line) { - echo str_pad($num+1, 5) . htmlentities($line, ENT_COMPAT, 'UTF-8'); - } - echo ''; - } - - $cache = $this->getPartialCacheStore(); - $scope = new SSViewer_Scope($model, $overlay, $underlay, $inheritedScope); - $val = ''; - - // Placeholder for values exposed to $cacheFile - [$cache, $scope, $val]; - include($cacheFile); - - return $val; - } - - /** - * Get the appropriate template to use for the named sub-template, or null if none are appropriate - */ - protected function getSubtemplateFor(string $subtemplate): ?array - { - // Get explicit subtemplate name - if (isset($this->subTemplates[$subtemplate])) { - return $this->subTemplates[$subtemplate]; - } - - // Don't apply sub-templates if type is already specified (e.g. 'Includes') - if (isset($this->templateCandidates['type'])) { - return null; - } - - // Filter out any other typed templates as we can only add, not change type - $templates = array_filter( - (array) $this->templateCandidates, - function ($template) { - return !isset($template['type']); - } - ); - if (empty($templates)) { - return null; - } - - // Set type to subtemplate - $templates['type'] = $subtemplate; - return $templates; - } - - /** - * Parse given template contents - * - * @param string $content The template contents - * @param string $template The template file name - */ - protected function parseTemplateContent(string $content, string $template = ""): string - { - return $this->getParser()->compileString( - $content, - $template, - Director::isDev() && SSViewer::config()->uninherited('source_file_comments') - ); - } - - /** - * Attempts to find possible candidate templates from a set of template - * names from modules, current theme directory and finally the application - * folder. - * - * The template names can be passed in as plain strings, or be in the - * format "type/name", where type is the type of template to search for - * (e.g. Includes, Layout). - * - * The results of this method will be cached for future use. - * - * @param string|array $template Template name, or template spec in array format with the keys - * 'type' (type string) and 'templates' (template hierarchy in order of precedence). - * If 'templates' is omitted then any other item in the array will be treated as the template - * list, or list of templates each in the array spec given. - * Templates with an .ss extension will be treated as file paths, and will bypass - * theme-coupled resolution. - * @param array $themes List of themes to use to resolve themes. Defaults to {@see SSViewer::get_themes()} - * @return string Absolute path to resolved template file, or null if not resolved. - * File location will be in the format themes/
$SSTemplateEngineTest_GlobalReturnsNull
')); - $this->assertEquals('', $this->render('$SSTemplateEngineTest_GlobalReturnsNull.Chained.Properties
')); - } - - public function testCoreGlobalVariableCalls() - { - $this->assertEquals( - Director::absoluteBaseURL(), - $this->render('{$absoluteBaseURL}'), - 'Director::absoluteBaseURL can be called from within template' - ); - $this->assertEquals( - Director::absoluteBaseURL(), - $this->render('{$AbsoluteBaseURL}'), - 'Upper-case %AbsoluteBaseURL can be called from within template' - ); - - $this->assertEquals( - Director::is_ajax(), - $this->render('{$isAjax}'), - 'All variations of is_ajax result in the correct call' - ); - $this->assertEquals( - Director::is_ajax(), - $this->render('{$IsAjax}'), - 'All variations of is_ajax result in the correct call' - ); - $this->assertEquals( - Director::is_ajax(), - $this->render('{$is_ajax}'), - 'All variations of is_ajax result in the correct call' - ); - $this->assertEquals( - Director::is_ajax(), - $this->render('{$Is_ajax}'), - 'All variations of is_ajax result in the correct call' - ); - - $this->assertEquals( - i18n::get_locale(), - $this->render('{$i18nLocale}'), - 'i18n template functions result correct result' - ); - $this->assertEquals( - i18n::get_locale(), - $this->render('{$get_locale}'), - 'i18n template functions result correct result' - ); - - $this->assertEquals( - Security::getCurrentUser()->ID, - $this->render('{$CurrentMember.ID}'), - 'Member template functions result correct result' - ); - $this->assertEquals( - Security::getCurrentUser()->ID, - $this->render('{$CurrentUser.ID}'), - 'Member template functions result correct result' - ); - $this->assertEquals( - Security::getCurrentUser()->ID, - $this->render('{$currentMember.ID}'), - 'Member template functions result correct result' - ); - $this->assertEquals( - Security::getCurrentUser()->ID, - $this->render('{$currentUser.ID}'), - 'Member template functions result correct result' - ); - - $this->assertEquals( - SecurityToken::getSecurityID(), - $this->render('{$getSecurityID}'), - 'SecurityToken template functions result correct result' - ); - $this->assertEquals( - SecurityToken::getSecurityID(), - $this->render('{$SecurityID}'), - 'SecurityToken template functions result correct result' - ); - - $this->assertEquals( - Permission::check("ADMIN"), - (bool)$this->render('{$HasPerm(\'ADMIN\')}'), - 'Permissions template functions result correct result' - ); - $this->assertEquals( - Permission::check("ADMIN"), - (bool)$this->render('{$hasPerm(\'ADMIN\')}'), - 'Permissions template functions result correct result' - ); - } - - public function testNonFieldCastingHelpersNotUsedInHasValue() - { - // check if Link without $ in front of variable - $result = $this->render( - 'A<% if Link %>$Link<% end_if %>B', - new SSTemplateEngineTest\TestObject() - ); - $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if Link %>'); - - // check if Link with $ in front of variable - $result = $this->render( - 'A<% if $Link %>$Link<% end_if %>B', - new SSTemplateEngineTest\TestObject() - ); - $this->assertEquals('Asome/url.htmlB', $result, 'casting helper not used for <% if $Link %>'); - } - - public function testLocalFunctionsTakePriorityOverGlobals() - { - $data = new ArrayData([ - 'Page' => new SSTemplateEngineTest\TestObject() - ]); - - //call method with lots of arguments - $result = $this->render( - '<% with Page %>$lotsOfArguments11("a","b","c","d","e","f","g","h","i","j","k")<% end_with %>', - $data - ); - $this->assertEquals("abcdefghijk", $result, "public function can accept up to 11 arguments"); - - //call method that does not exist - $result = $this->render('<% with Page %><% if IDoNotExist %>hello<% end_if %><% end_with %>', $data); - $this->assertEquals("", $result, "Method does not exist - empty result"); - - //call if that does not exist - $result = $this->render('<% with Page %>$IDoNotExist("hello")<% end_with %>', $data); - $this->assertEquals("", $result, "Method does not exist - empty result"); - - //call method with same name as a global method (local call should take priority) - $result = $this->render('<% with Page %>$absoluteBaseURL<% end_with %>', $data); - $this->assertEquals( - "testLocalFunctionPriorityCalled", - $result, - "Local Object's public function called. Did not return the actual baseURL of the current site" - ); - } - - public function testCurrentScopeLoop(): void - { - $data = new ArrayList([['Val' => 'one'], ['Val' => 'two'], ['Val' => 'three']]); - $this->assertEqualIgnoringWhitespace( - 'one two three', - $this->render('<% loop %>$Val<% end_loop %>', $data) - ); - } - - public function testCurrentScopeLoopWith() - { - // Data to run the loop tests on - one sequence of three items, each with a subitem - $data = new ArrayData([ - 'Foo' => new ArrayList([ - 'Subocean' => new ArrayData([ - 'Name' => 'Higher' - ]), - new ArrayData([ - 'Sub' => new ArrayData([ - 'Name' => 'SubKid1' - ]) - ]), - new ArrayData([ - 'Sub' => new ArrayData([ - 'Name' => 'SubKid2' - ]) - ]), - new SSTemplateEngineTest\TestObject('Number6') - ]) - ]); - - $result = $this->render( - '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>', - $data - ); - $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works"); - - $result = $this->render( - '<% loop Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %><% end_if %><% end_loop %>', - $data - ); - $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop works"); - - $result = $this->render('<% with Foo %>$Count<% end_with %>', $data); - $this->assertEquals("4", $result, "4 items in the DataObjectSet"); - - $result = $this->render( - '<% with Foo %><% loop Up.Foo %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>' - . '<% end_if %><% end_loop %><% end_with %>', - $data - ); - $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in with Up.Foo scope works"); - - $result = $this->render( - '<% with Foo %><% loop %>$Number<% if Sub %><% with Sub %>$Name<% end_with %>' - . '<% end_if %><% end_loop %><% end_with %>', - $data - ); - $this->assertEquals("SubKid1SubKid2Number6", $result, "Loop in current scope works"); - } - - public static function provideArgumentTypes() - { - return [ - [ - 'arg0:0,arg1:"string",arg2:true', - '$methodWithTypedArguments(0, "string", true).RAW', - ], - [ - 'arg0:false,arg1:"string",arg2:true', - '$methodWithTypedArguments(false, "string", true).RAW', - ], - [ - 'arg0:null,arg1:"string",arg2:true', - '$methodWithTypedArguments(null, "string", true).RAW', - ], - [ - 'arg0:"",arg1:"string",arg2:true', - '$methodWithTypedArguments("", "string", true).RAW', - ], - [ - 'arg0:0,arg1:1,arg2:2', - '$methodWithTypedArguments(0, 1, 2).RAW', - ], - ]; - } - - #[DataProvider('provideArgumentTypes')] - public function testArgumentTypes(string $expected, string $template) - { - $this->assertEquals($expected, $this->render($template, new TestModelData())); - } - - public static function provideEvaluatedArgumentTypes(): array - { - $stdobj = new stdClass(); - $stdobj->key = 'value'; - $scenarios = [ - 'null value' => [ - 'data' => ['Value' => null], - 'useOverlay' => true, - 'expected' => 'arg0:null', - ], - 'int value' => [ - 'data' => ['Value' => 1], - 'useOverlay' => true, - 'expected' => 'arg0:1', - ], - 'string value' => [ - 'data' => ['Value' => '1'], - 'useOverlay' => true, - 'expected' => 'arg0:"1"', - ], - 'boolean true' => [ - 'data' => ['Value' => true], - 'useOverlay' => true, - 'expected' => 'arg0:true', - ], - 'boolean false' => [ - 'data' => ['Value' => false], - 'useOverlay' => true, - 'expected' => 'arg0:false', - ], - 'object value' => [ - 'data' => ['Value' => $stdobj], - 'useOverlay' => true, - 'expected' => 'arg0:{"key":"value"}', - ], - ]; - foreach ($scenarios as $key => $scenario) { - $scenario['useOverlay'] = false; - $scenarios[$key . ' no overlay'] = $scenario; - } - return $scenarios; - } - - #[DataProvider('provideEvaluatedArgumentTypes')] - public function testEvaluatedArgumentTypes(array $data, bool $useOverlay, string $expected): void - { - $template = '$methodWithTypedArguments($Value).RAW'; - $model = new TestModelData(); - $overlay = $data; - if (!$useOverlay) { - $model = $model->customise($data); - $overlay = []; - } - $this->assertEquals($expected, $this->render($template, $model, $overlay)); - } - - public function testObjectDotArguments() - { - $this->assertEquals( - '[out:TestObject.methodWithOneArgument(one)] - [out:TestObject.methodWithTwoArguments(one,two)] - [out:TestMethod(Arg1,Arg2).Bar.Val] - [out:TestMethod(Arg1,Arg2).Bar] - [out:TestMethod(Arg1,Arg2)] - [out:TestMethod(Arg1).Bar.Val] - [out:TestMethod(Arg1).Bar] - [out:TestMethod(Arg1)]', - $this->render( - '$TestObject.methodWithOneArgument(one) - $TestObject.methodWithTwoArguments(one,two) - $TestMethod(Arg1, Arg2).Bar.Val - $TestMethod(Arg1, Arg2).Bar - $TestMethod(Arg1, Arg2) - $TestMethod(Arg1).Bar.Val - $TestMethod(Arg1).Bar - $TestMethod(Arg1)' - ) - ); - } - - public function testEscapedArguments() - { - $this->assertEquals( - '[out:Foo(Arg1,Arg2).Bar.Val].Suffix - [out:Foo(Arg1,Arg2).Val]_Suffix - [out:Foo(Arg1,Arg2)]/Suffix - [out:Foo(Arg1).Bar.Val]textSuffix - [out:Foo(Arg1).Bar].Suffix - [out:Foo(Arg1)].Suffix - [out:Foo.Bar.Val].Suffix - [out:Foo.Bar].Suffix - [out:Foo].Suffix', - $this->render( - '{$Foo(Arg1, Arg2).Bar.Val}.Suffix - {$Foo(Arg1, Arg2).Val}_Suffix - {$Foo(Arg1, Arg2)}/Suffix - {$Foo(Arg1).Bar.Val}textSuffix - {$Foo(Arg1).Bar}.Suffix - {$Foo(Arg1)}.Suffix - {$Foo.Bar.Val}.Suffix - {$Foo.Bar}.Suffix - {$Foo}.Suffix' - ) - ); - } - - public function testLoopWhitespace() - { - $data = new ArrayList([new SSTemplateEngineTest\TestFixture()]); - $this->assertEquals( - 'before[out:Test]after - beforeTestafter', - $this->render( - 'before<% loop %>$Test<% end_loop %>after - before<% loop %>Test<% end_loop %>after', - $data - ) - ); - - // The control tags are removed from the output, but no whitespace - // This is a quirk that could be changed, but included in the test to make the current - // behaviour explicit - $this->assertEquals( - 'before - -[out:ItemOnItsOwnLine] - -after', - $this->render( - 'before -<% loop %> -$ItemOnItsOwnLine -<% end_loop %> -after', - $data - ) - ); - - // The whitespace within the control tags is preserve in a loop - // This is a quirk that could be changed, but included in the test to make the current - // behaviour explicit - $this->assertEquals( - 'before - -[out:Loop3.ItemOnItsOwnLine] - -[out:Loop3.ItemOnItsOwnLine] - -[out:Loop3.ItemOnItsOwnLine] - -after', - $this->render( - 'before -<% loop Loop3 %> -$ItemOnItsOwnLine -<% end_loop %> -after' - ) - ); - } - - public static function typePreservationDataProvider() - { - return [ - // Null - ['NULL:', 'null'], - ['NULL:', 'NULL'], - // Booleans - ['boolean:1', 'true'], - ['boolean:1', 'TRUE'], - ['boolean:', 'false'], - ['boolean:', 'FALSE'], - // Strings which may look like booleans/null to the parser - ['string:nullish', 'nullish'], - ['string:notnull', 'notnull'], - ['string:truethy', 'truethy'], - ['string:untrue', 'untrue'], - ['string:falsey', 'falsey'], - // Integers - ['integer:0', '0'], - ['integer:1', '1'], - ['integer:15', '15'], - ['integer:-15', '-15'], - // Octal integers - ['integer:83', '0123'], - ['integer:-83', '-0123'], - // Hexadecimal integers - ['integer:26', '0x1A'], - ['integer:-26', '-0x1A'], - // Binary integers - ['integer:255', '0b11111111'], - ['integer:-255', '-0b11111111'], - // Floats (aka doubles) - ['double:0', '0.0'], - ['double:1', '1.0'], - ['double:15.25', '15.25'], - ['double:-15.25', '-15.25'], - ['double:1200', '1.2e3'], - ['double:-1200', '-1.2e3'], - ['double:0.07', '7E-2'], - ['double:-0.07', '-7E-2'], - // Explicitly quoted strings - ['string:0', '"0"'], - ['string:1', '\'1\''], - ['string:foobar', '"foobar"'], - ['string:foo bar baz', '"foo bar baz"'], - ['string:false', '\'false\''], - ['string:true', '\'true\''], - ['string:null', '\'null\''], - ['string:false', '"false"'], - ['string:true', '"true"'], - ['string:null', '"null"'], - // Implicit strings - ['string:foobar', 'foobar'], - ['string:foo bar baz', 'foo bar baz'] - ]; - } - - #[DataProvider('typePreservationDataProvider')] - public function testTypesArePreserved($expected, $templateArg) - { - $data = new ArrayData([ - 'Test' => new TestModelData() - ]); - - $this->assertEquals($expected, $this->render("\$Test.Type({$templateArg})", $data)); - } - - #[DataProvider('typePreservationDataProvider')] - public function testTypesArePreservedAsIncludeArguments($expected, $templateArg) - { - $data = new ArrayData([ - 'Test' => new TestModelData() - ]); - - $this->assertEquals( - $expected, - $this->render("<% include SSTemplateEngineTestTypePreservation Argument={$templateArg} %>", $data) - ); - } - - public function testTypePreservationInConditionals() - { - $data = new ArrayData([ - 'Test' => new TestModelData() - ]); - - // Types in conditionals - $this->assertEquals('pass', $this->render('<% if true %>pass<% else %>fail<% end_if %>', $data)); - $this->assertEquals('pass', $this->render('<% if false %>fail<% else %>pass<% end_if %>', $data)); - $this->assertEquals('pass', $this->render('<% if 1 %>pass<% else %>fail<% end_if %>', $data)); - $this->assertEquals('pass', $this->render('<% if 0 %>fail<% else %>pass<% end_if %>', $data)); - } - - public function testControls() - { - // Single item controls - $this->assertEquals( - 'a[out:Foo.Bar.Item]b - [out:Foo.Bar(Arg1).Item] - [out:Foo(Arg1).Item] - [out:Foo(Arg1,Arg2).Item] - [out:Foo(Arg1,Arg2,Arg3).Item]', - $this->render( - '<% with Foo.Bar %>a{$Item}b<% end_with %> - <% with Foo.Bar(Arg1) %>$Item<% end_with %> - <% with Foo(Arg1) %>$Item<% end_with %> - <% with Foo(Arg1, Arg2) %>$Item<% end_with %> - <% with Foo(Arg1, Arg2, Arg3) %>$Item<% end_with %>' - ) - ); - - // Loop controls - $this->assertEquals( - 'a[out:Foo.Loop2.Item]ba[out:Foo.Loop2.Item]b', - $this->render('<% loop Foo.Loop2 %>a{$Item}b<% end_loop %>') - ); - - $this->assertEquals( - '[out:Foo.Loop2(Arg1).Item][out:Foo.Loop2(Arg1).Item]', - $this->render('<% loop Foo.Loop2(Arg1) %>$Item<% end_loop %>') - ); - - $this->assertEquals( - '[out:Loop2(Arg1).Item][out:Loop2(Arg1).Item]', - $this->render('<% loop Loop2(Arg1) %>$Item<% end_loop %>') - ); - - $this->assertEquals( - '[out:Loop2(Arg1,Arg2).Item][out:Loop2(Arg1,Arg2).Item]', - $this->render('<% loop Loop2(Arg1, Arg2) %>$Item<% end_loop %>') - ); - - $this->assertEquals( - '[out:Loop2(Arg1,Arg2,Arg3).Item][out:Loop2(Arg1,Arg2,Arg3).Item]', - $this->render('<% loop Loop2(Arg1, Arg2, Arg3) %>$Item<% end_loop %>') - ); - } - - public function testIfBlocks() - { - // Basic test - $this->assertEquals( - 'AC', - $this->render('A<% if NotSet %>B$NotSet<% end_if %>C') - ); - - // Nested test - $this->assertEquals( - 'AB1C', - $this->render('A<% if IsSet %>B$NotSet<% if IsSet %>1<% else %>2<% end_if %><% end_if %>C') - ); - - // else_if - $this->assertEquals( - 'ACD', - $this->render('A<% if NotSet %>B<% else_if IsSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% end_if %>D') - ); - $this->assertEquals( - 'ADE', - $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E') - ); - - $this->assertEquals( - 'ADE', - $this->render('A<% if NotSet %>B<% else_if AlsoNotset %>C<% else_if IsSet %>D<% end_if %>E') - ); - - // Dot syntax - $this->assertEquals( - 'ACD', - $this->render('A<% if Foo.NotSet %>B<% else_if Foo.IsSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if Foo.Bar.NotSet %>B<% else_if Foo.Bar.IsSet %>C<% end_if %>D') - ); - - // Params - $this->assertEquals( - 'ACD', - $this->render('A<% if NotSet(Param) %>B<% else %>C<% end_if %>D') - ); - $this->assertEquals( - 'ABD', - $this->render('A<% if IsSet(Param) %>B<% else %>C<% end_if %>D') - ); - - // Negation - $this->assertEquals( - 'AC', - $this->render('A<% if not IsSet %>B<% end_if %>C') - ); - $this->assertEquals( - 'ABC', - $this->render('A<% if not NotSet %>B<% end_if %>C') - ); - - // Or - $this->assertEquals( - 'ABD', - $this->render('A<% if IsSet || NotSet %>B<% else_if A %>C<% end_if %>D') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet3 %>C<% end_if %>D') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if IsSet || NotSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet || AlsoNotSet %>B<% else_if NotSet2 || NotSet3 %>C<% end_if %>D') - ); - - // Negated Or - $this->assertEquals( - 'ACD', - $this->render('A<% if not IsSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D') - ); - $this->assertEquals( - 'ABD', - $this->render('A<% if not NotSet || AlsoNotSet %>B<% else_if A %>C<% end_if %>D') - ); - $this->assertEquals( - 'ABD', - $this->render('A<% if NotSet || not AlsoNotSet %>B<% else_if A %>C<% end_if %>D') - ); - - // And - $this->assertEquals( - 'ABD', - $this->render('A<% if IsSet && AlsoSet %>B<% else_if A %>C<% end_if %>D') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet && NotSet2 %>B<% else_if NotSet3 %>C<% end_if %>D') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if IsSet && NotSet %>B<% else_if IsSet && AlsoSet %>C<% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet && NotSet2 %>B<% else_if IsSet && NotSet3 %>C<% end_if %>D') - ); - - // Equality - $this->assertEquals( - 'ABC', - $this->render('A<% if RawVal == RawVal %>B<% end_if %>C') - ); - $this->assertEquals( - 'ACD', - $this->render('A<% if Right == Wrong %>B<% else_if RawVal == RawVal %>C<% end_if %>D') - ); - $this->assertEquals( - 'ABC', - $this->render('A<% if Right != Wrong %>B<% end_if %>C') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% end_if %>D') - ); - - // test inequalities with simple numbers - $this->assertEquals('ABD', $this->render('A<% if 5 > 3 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ABD', $this->render('A<% if 5 >= 3 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 3 > 5 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 3 >= 5 %>B<% else %>C<% end_if %>D')); - - $this->assertEquals('ABD', $this->render('A<% if 3 < 5 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ABD', $this->render('A<% if 3 <= 5 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 5 < 3 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 5 <= 3 %>B<% else %>C<% end_if %>D')); - - $this->assertEquals('ABD', $this->render('A<% if 4 <= 4 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ABD', $this->render('A<% if 4 >= 4 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 4 > 4 %>B<% else %>C<% end_if %>D')); - $this->assertEquals('ACD', $this->render('A<% if 4 < 4 %>B<% else %>C<% end_if %>D')); - - // empty else_if and else tags, if this would not be supported, - // the output would stop after A, thereby failing the assert - $this->assertEquals('AD', $this->render('A<% if IsSet %><% else %><% end_if %>D')); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet %><% else_if IsSet %><% else %><% end_if %>D') - ); - $this->assertEquals( - 'AD', - $this->render('A<% if NotSet %><% else_if AlsoNotSet %><% else %><% end_if %>D') - ); - - // Bare words with ending space - $this->assertEquals( - 'ABC', - $this->render('A<% if "RawVal" == RawVal %>B<% end_if %>C') - ); - - // Else - $this->assertEquals( - 'ADE', - $this->render('A<% if Right == Wrong %>B<% else_if RawVal != RawVal %>C<% else %>D<% end_if %>E') - ); - - // Empty if with else - $this->assertEquals( - 'ABC', - $this->render('A<% if NotSet %><% else %>B<% end_if %>C') - ); - } - - public static function provideIfBlockWithIterable(): array - { - $scenarios = [ - 'empty array' => [ - 'iterable' => [], - 'inScope' => false, - ], - 'array' => [ - 'iterable' => [1, 2, 3], - 'inScope' => false, - ], - 'ArrayList' => [ - 'iterable' => new ArrayList([['Val' => 1], ['Val' => 2], ['Val' => 3]]), - 'inScope' => false, - ], - ]; - foreach ($scenarios as $name => $scenario) { - $scenario['inScope'] = true; - $scenarios[$name . ' in scope'] = $scenario; - } - return $scenarios; - } - - #[DataProvider('provideIfBlockWithIterable')] - public function testIfBlockWithIterable(iterable $iterable, bool $inScope): void - { - $expected = count($iterable) ? 'has value' : 'no value'; - $data = new ArrayData(['Iterable' => $iterable]); - if ($inScope) { - $template = '<% with $Iterable %><% if $Me %>has value<% else %>no value<% end_if %><% end_with %>'; - } else { - $template = '<% if $Iterable %>has value<% else %>no value<% end_if %>'; - } - $this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data)); - } - - public function testBaseTagGeneration() - { - // XHTML will have a closed base tag - $tmpl1 = ' - - - <% base_tag %> -test
- '; - $this->assertMatchesRegularExpression('/test
- '; - $this->assertMatchesRegularExpression( - '/test
- '; - $this->assertMatchesRegularExpression( - '/[out:Arg1]
[out:Arg2]
[out:Arg2.Count]
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments %>') - ); - - $this->assertEquals( - 'A
[out:Arg2]
[out:Arg2.Count]
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1=A %>') - ); - - $this->assertEquals( - 'A
B
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1=A, Arg2=B %>') - ); - - $this->assertEquals( - 'A Bare String
B Bare String
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1=A Bare String, Arg2=B Bare String %>') - ); - - $this->assertEquals( - 'A
Bar
', - $this->render( - '<% include SSTemplateEngineTestIncludeWithArguments Arg1="A", Arg2=$B %>', - new ArrayData(['B' => 'Bar']) - ) - ); - - $this->assertEquals( - 'A
Bar
', - $this->render( - '<% include SSTemplateEngineTestIncludeWithArguments Arg1="A" %>', - new ArrayData(['Arg1' => 'Foo', 'Arg2' => 'Bar']) - ) - ); - - $this->assertEquals( - 'A
0
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1="A", Arg2=0 %>') - ); - - $this->assertEquals( - 'A
', - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1="A", Arg2=false %>') - ); - - $this->assertEquals( - 'A
', - // Note Arg2 is explicitly overridden with null - $this->render('<% include SSTemplateEngineTestIncludeWithArguments Arg1="A", Arg2=null %>') - ); - - $this->assertEquals( - 'SomeArg - Foo - Bar - SomeArg', - $this->render( - '<% include SSTemplateEngineTestIncludeScopeInheritanceWithArgsInLoop Title="SomeArg" %>', - new ArrayData( - ['Items' => new ArrayList( - [ - new ArrayData(['Title' => 'Foo']), - new ArrayData(['Title' => 'Bar']) - ] - )] - ) - ) - ); - - $this->assertEquals( - 'A - B - A', - $this->render( - '<% include SSTemplateEngineTestIncludeScopeInheritanceWithArgsInWith Title="A" %>', - new ArrayData(['Item' => new ArrayData(['Title' =>'B'])]) - ) - ); - - $this->assertEquals( - 'A - B - C - B - A', - $this->render( - '<% include SSTemplateEngineTestIncludeScopeInheritanceWithArgsInNestedWith Title="A" %>', - new ArrayData( - [ - 'Item' => new ArrayData( - [ - 'Title' =>'B', 'NestedItem' => new ArrayData(['Title' => 'C']) - ] - )] - ) - ) - ); - - $this->assertEquals( - 'A - A - A', - $this->render( - '<% include SSTemplateEngineTestIncludeScopeInheritanceWithUpAndTop Title="A" %>', - new ArrayData( - [ - 'Item' => new ArrayData( - [ - 'Title' =>'B', 'NestedItem' => new ArrayData(['Title' => 'C']) - ] - )] - ) - ) - ); - - $data = new ArrayData( - [ - 'Nested' => new ArrayData( - [ - 'Object' => new ArrayData(['Key' => 'A']) - ] - ), - 'Object' => new ArrayData(['Key' => 'B']) - ] - ); - - $res = $this->render('<% include SSTemplateEngineTestIncludeObjectArguments A=$Nested.Object, B=$Object %>', $data); - $this->assertEqualIgnoringWhitespace('A B', $res, 'Objects can be passed as named arguments'); - } - - public function testNamespaceInclude() - { - $data = new ArrayData([]); - - $this->assertEquals( - "tests:( NamespaceInclude\n )", - $this->render('tests:( <% include Namespace\NamespaceInclude %> )', $data), - 'Backslashes work for namespace references in includes' - ); - - $this->assertEquals( - "tests:( NamespaceInclude\n )", - $this->render('tests:( <% include Namespace\\NamespaceInclude %> )', $data), - 'Escaped backslashes work for namespace references in includes' - ); - - $this->assertEquals( - "tests:( NamespaceInclude\n )", - $this->render('tests:( <% include Namespace/NamespaceInclude %> )', $data), - 'Forward slashes work for namespace references in includes' - ); - } - - /** - * Test search for includes fallback to non-includes folder - */ - public function testIncludeFallbacks() - { - $data = new ArrayData([]); - - $this->assertEquals( - "tests:( Namespace/Includes/IncludedTwice.ss\n )", - $this->render('tests:( <% include Namespace\\IncludedTwice %> )', $data), - 'Prefer Includes in the Includes folder' - ); - - $this->assertEquals( - "tests:( Namespace/Includes/IncludedOnceSub.ss\n )", - $this->render('tests:( <% include Namespace\\IncludedOnceSub %> )', $data), - 'Includes in only Includes folder can be found' - ); - - $this->assertEquals( - "tests:( Namespace/IncludedOnceBase.ss\n )", - $this->render('tests:( <% include Namespace\\IncludedOnceBase %> )', $data), - 'Includes outside of Includes folder can be found' - ); - } - - public function testRecursiveInclude() - { - $data = new ArrayData( - [ - 'Title' => 'A', - 'Children' => new ArrayList( - [ - new ArrayData( - [ - 'Title' => 'A1', - 'Children' => new ArrayList( - [ - new ArrayData([ 'Title' => 'A1 i', ]), - new ArrayData([ 'Title' => 'A1 ii', ]), - ] - ), - ] - ), - new ArrayData([ 'Title' => 'A2', ]), - new ArrayData([ 'Title' => 'A3', ]), - ] - ), - ] - ); - - $engine = new SSTemplateEngine('Includes/SSTemplateEngineTestRecursiveInclude'); - $result = $engine->render(new ViewLayerData($data)); - // We don't care about whitespace - $rationalisedResult = trim(preg_replace('/\s+/', ' ', $result ?? '') ?? ''); - - $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult); - } - - /** - * See {@link ModelDataTest} for more extensive casting tests, - * this test just ensures that basic casting is correctly applied during template parsing. - */ - public function testCastingHelpers() - { - $vd = new SSTemplateEngineTest\TestModelData(); - $vd->TextValue = 'html'; - $vd->HTMLValue = 'html'; - $vd->UncastedValue = 'html'; - - // Value casted as "Text" - $this->assertEquals( - '<b>html</b>', - $this->render('$TextValue', $vd) - ); - $this->assertEquals( - 'html', - $this->render('$TextValue.RAW', $vd) - ); - $this->assertEquals( - '<b>html</b>', - $this->render('$TextValue.XML', $vd) - ); - - // Value casted as "HTMLText" - $this->assertEquals( - 'html', - $this->render('$HTMLValue', $vd) - ); - $this->assertEquals( - 'html', - $this->render('$HTMLValue.RAW', $vd) - ); - $this->assertEquals( - '<b>html</b>', - $this->render('$HTMLValue.XML', $vd) - ); - - // Uncasted value (falls back to the relevant DBField class for the data type) - $vd = new SSTemplateEngineTest\TestModelData(); - $vd->UncastedValue = 'html'; - $this->assertEquals( - '<b>html</b>', - $this->render('$UncastedValue', $vd) - ); - $this->assertEquals( - 'html', - $this->render('$UncastedValue.RAW', $vd) - ); - $this->assertEquals( - '<b>html</b>', - $this->render('$UncastedValue.XML', $vd) - ); - } - - public static function provideLoop(): array - { - return [ - 'nested array and iterator' => [ - 'iterable' => [ - [ - 'value 1', - 'value 2', - ], - new ArrayList([ - 'value 3', - 'value 4', - ]), - ], - 'template' => '<% loop $Iterable %><% loop $Me %>$Me<% end_loop %><% end_loop %>', - 'expected' => 'value 1 value 2 value 3 value 4', - ], - 'nested associative arrays' => [ - 'iterable' => [ - [ - 'Foo' => 'one', - ], - [ - 'Foo' => 'two', - ], - [ - 'Foo' => 'three', - ], - ], - 'template' => '<% loop $Iterable %>$Foo<% end_loop %>', - 'expected' => 'one two three', - ], - ]; - } - - #[DataProvider('provideLoop')] - public function testLoop(iterable $iterable, string $template, string $expected): void - { - $data = new ArrayData(['Iterable' => $iterable]); - $this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data)); - } - - public static function provideCountIterable(): array - { - $scenarios = [ - 'empty array' => [ - 'iterable' => [], - 'inScope' => false, - ], - 'array' => [ - 'iterable' => [1, 2, 3], - 'inScope' => false, - ], - 'ArrayList' => [ - 'iterable' => new ArrayList([['Val' => 1], ['Val' => 2], ['Val' => 3]]), - 'inScope' => false, - ], - ]; - foreach ($scenarios as $name => $scenario) { - $scenario['inScope'] = true; - $scenarios[$name . ' in scope'] = $scenario; - } - return $scenarios; - } - - #[DataProvider('provideCountIterable')] - public function testCountIterable(iterable $iterable, bool $inScope): void - { - $expected = count($iterable); - $data = new ArrayData(['Iterable' => $iterable]); - if ($inScope) { - $template = '<% with $Iterable %>$Count<% end_with %>'; - } else { - $template = '$Iterable.Count'; - } - $this->assertEqualIgnoringWhitespace($expected, $this->render($template, $data)); - } - - public function testSSViewerBasicIteratorSupport() - { - $data = new ArrayData( - [ - 'Set' => new ArrayList( - [ - new SSTemplateEngineTest\TestObject("1"), - new SSTemplateEngineTest\TestObject("2"), - new SSTemplateEngineTest\TestObject("3"), - new SSTemplateEngineTest\TestObject("4"), - new SSTemplateEngineTest\TestObject("5"), - new SSTemplateEngineTest\TestObject("6"), - new SSTemplateEngineTest\TestObject("7"), - new SSTemplateEngineTest\TestObject("8"), - new SSTemplateEngineTest\TestObject("9"), - new SSTemplateEngineTest\TestObject("10"), - ] - ) - ] - ); - - //base test - $result = $this->render('<% loop Set %>$Number<% end_loop %>', $data); - $this->assertEquals("12345678910", $result, "Numbers rendered in order"); - - //test First - $result = $this->render('<% loop Set %><% if $IsFirst %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("1", $result, "Only the first number is rendered"); - - //test Last - $result = $this->render('<% loop Set %><% if $IsLast %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("10", $result, "Only the last number is rendered"); - - //test Even - $result = $this->render('<% loop Set %><% if $Even() %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("246810", $result, "Even numbers rendered in order"); - - //test Even with quotes - $result = $this->render('<% loop Set %><% if $Even("1") %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("246810", $result, "Even numbers rendered in order"); - - //test Even without quotes - $result = $this->render('<% loop Set %><% if $Even(1) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("246810", $result, "Even numbers rendered in order"); - - //test Even with zero-based start index - $result = $this->render('<% loop Set %><% if $Even("0") %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("13579", $result, "Even (with zero-based index) numbers rendered in order"); - - //test Odd - $result = $this->render('<% loop Set %><% if $Odd %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("13579", $result, "Odd numbers rendered in order"); - - //test FirstLast - $result = $this->render('<% loop Set %><% if $FirstLast %>$Number$FirstLast<% end_if %><% end_loop %>', $data); - $this->assertEquals("1first10last", $result, "First and last numbers rendered in order"); - - //test Middle - $result = $this->render('<% loop Set %><% if $Middle %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("23456789", $result, "Middle numbers rendered in order"); - - //test MiddleString - $result = $this->render( - '<% loop Set %><% if MiddleString == "middle" %>$Number$MiddleString<% end_if %>' - . '<% end_loop %>', - $data - ); - $this->assertEquals( - "2middle3middle4middle5middle6middle7middle8middle9middle", - $result, - "Middle numbers rendered in order" - ); - - //test EvenOdd - $result = $this->render('<% loop Set %>$EvenOdd<% end_loop %>', $data); - $this->assertEquals( - "oddevenoddevenoddevenoddevenoddeven", - $result, - "Even and Odd is returned in sequence numbers rendered in order" - ); - - //test Pos - $result = $this->render('<% loop Set %>$Pos<% end_loop %>', $data); - $this->assertEquals("12345678910", $result, '$Pos is rendered in order'); - - //test Pos - $result = $this->render('<% loop Set %>$Pos(0)<% end_loop %>', $data); - $this->assertEquals("0123456789", $result, '$Pos(0) is rendered in order'); - - //test FromEnd - $result = $this->render('<% loop Set %>$FromEnd<% end_loop %>', $data); - $this->assertEquals("10987654321", $result, '$FromEnd is rendered in order'); - - //test FromEnd - $result = $this->render('<% loop Set %>$FromEnd(0)<% end_loop %>', $data); - $this->assertEquals("9876543210", $result, '$FromEnd(0) rendered in order'); - - //test Total - $result = $this->render('<% loop Set %>$TotalItems<% end_loop %>', $data); - $this->assertEquals("10101010101010101010", $result, "10 total items X 10 are returned"); - - //test Modulus - $result = $this->render('<% loop Set %>$Modulus(2,1)<% end_loop %>', $data); - $this->assertEquals("1010101010", $result, "1-indexed pos modular divided by 2 rendered in order"); - - //test MultipleOf 3 - $result = $this->render('<% loop Set %><% if MultipleOf(3) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("369", $result, "Only numbers that are multiples of 3 are returned"); - - //test MultipleOf 4 - $result = $this->render('<% loop Set %><% if MultipleOf(4) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("48", $result, "Only numbers that are multiples of 4 are returned"); - - //test MultipleOf 5 - $result = $this->render('<% loop Set %><% if MultipleOf(5) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("510", $result, "Only numbers that are multiples of 5 are returned"); - - //test MultipleOf 10 - $result = $this->render('<% loop Set %><% if MultipleOf(10,1) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("10", $result, "Only numbers that are multiples of 10 (with 1-based indexing) are returned"); - - //test MultipleOf 9 zero-based - $result = $this->render('<% loop Set %><% if MultipleOf(9,0) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals( - "110", - $result, - "Only numbers that are multiples of 9 with zero-based indexing are returned. (The first and last item)" - ); - - //test MultipleOf 11 - $result = $this->render('<% loop Set %><% if MultipleOf(11) %>$Number<% end_if %><% end_loop %>', $data); - $this->assertEquals("", $result, "Only numbers that are multiples of 11 are returned. I.e. nothing returned"); - } - - /** - * Test $Up works when the scope $Up refers to was entered with a "with" block - */ - public function testUpInWith() - { - - // Data to run the loop tests on - three levels deep - $data = new ArrayData( - [ - 'Name' => 'Top', - 'Foo' => new ArrayData( - [ - 'Name' => 'Foo', - 'Bar' => new ArrayData( - [ - 'Name' => 'Bar', - 'Baz' => new ArrayData( - [ - 'Name' => 'Baz' - ] - ), - 'Qux' => new ArrayData( - [ - 'Name' => 'Qux' - ] - ) - ] - ) - ] - ) - ] - ); - - // Basic functionality - $this->assertEquals( - 'BarFoo', - $this->render('<% with Foo %><% with Bar %>{$Name}{$Up.Name}<% end_with %><% end_with %>', $data) - ); - - // Two level with block, up refers to internally referenced Bar - $this->assertEquals( - 'BarTop', - $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data) - ); - - // Stepping up & back down the scope tree - $this->assertEquals( - 'BazFooBar', - $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Foo.Name}{$Up.Foo.Bar.Name}<% end_with %>', $data) - ); - - // Using $Up in a with block - $this->assertEquals( - 'BazTopBar', - $this->render( - '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Foo.Bar.Name}<% end_with %>' - . '<% end_with %>', - $data - ) - ); - - // Stepping up & back down the scope tree with with blocks - $this->assertEquals( - 'BazTopBarTopBaz', - $this->render( - '<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Foo.Bar %>{$Name}<% end_with %>' - . '{$Name}<% end_with %>{$Name}<% end_with %>', - $data - ) - ); - - // Using $Up.Up, where first $Up points to a previous scope entered using $Up, thereby skipping up to Foo - $this->assertEquals( - 'Foo', - $this->render( - '<% with Foo %><% with Bar %><% with Baz %>{$Up.Up.Name}<% end_with %><% end_with %>' - . '<% end_with %>', - $data - ) - ); - - // Using $Up as part of a lookup chain in <% with %> - $this->assertEquals( - 'Top', - $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Name}<% end_with %>', $data) - ); - } - - public function testTooManyUps() - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage("Up called when we're already at the top of the scope"); - $data = new ArrayData([ - 'Foo' => new ArrayData([ - 'Name' => 'Foo', - 'Bar' => new ArrayData([ - 'Name' => 'Bar' - ]) - ]) - ]); - - $this->assertEquals( - 'Foo', - $this->render('<% with Foo.Bar %>{$Up.Up.Name}<% end_with %>', $data) - ); - } - - /** - * Test $Up works when the scope $Up refers to was entered with a "loop" block - */ - public function testUpInLoop() - { - - // Data to run the loop tests on - one sequence of three items, each with a subitem - $data = new ArrayData( - [ - 'Name' => 'Top', - 'Foo' => new ArrayList( - [ - new ArrayData( - [ - 'Name' => '1', - 'Sub' => new ArrayData( - [ - 'Name' => 'Bar' - ] - ) - ] - ), - new ArrayData( - [ - 'Name' => '2', - 'Sub' => new ArrayData( - [ - 'Name' => 'Baz' - ] - ) - ] - ), - new ArrayData( - [ - 'Name' => '3', - 'Sub' => new ArrayData( - [ - 'Name' => 'Qux' - ] - ) - ] - ) - ] - ) - ] - ); - - // Make sure inside a loop, $Up refers to the current item of the loop - $this->assertEqualIgnoringWhitespace( - '111 222 333', - $this->render( - '<% loop $Foo %>$Name<% with $Sub %>$Up.Name<% end_with %>$Name<% end_loop %>', - $data - ) - ); - - // Make sure inside a loop, looping over $Up uses a separate iterator, - // and doesn't interfere with the original iterator - $this->assertEqualIgnoringWhitespace( - '1Bar123Bar1 2Baz123Baz2 3Qux123Qux3', - $this->render( - '<% loop $Foo %> - $Name - <% with $Sub %> - $Name - <% loop $Up %>$Name<% end_loop %> - $Name - <% end_with %> - $Name - <% end_loop %>', - $data - ) - ); - - // Make sure inside a loop, looping over $Up uses a separate iterator, - // and doesn't interfere with the original iterator or local lookups - $this->assertEqualIgnoringWhitespace( - '1 Bar1 123 1Bar 1 2 Baz2 123 2Baz 2 3 Qux3 123 3Qux 3', - $this->render( - '<% loop $Foo %> - $Name - <% with $Sub %> - {$Name}{$Up.Name} - <% loop $Up %>$Name<% end_loop %> - {$Up.Name}{$Name} - <% end_with %> - $Name - <% end_loop %>', - $data - ) - ); - } - - /** - * Test that nested loops restore the loop variables correctly when pushing and popping states - */ - public function testNestedLoops() - { - - // Data to run the loop tests on - one sequence of three items, one with child elements - // (of a different size to the main sequence) - $data = new ArrayData( - [ - 'Foo' => new ArrayList( - [ - new ArrayData( - [ - 'Name' => '1', - 'Children' => new ArrayList( - [ - new ArrayData( - [ - 'Name' => 'a' - ] - ), - new ArrayData( - [ - 'Name' => 'b' - ] - ), - ] - ), - ] - ), - new ArrayData( - [ - 'Name' => '2', - 'Children' => new ArrayList(), - ] - ), - new ArrayData( - [ - 'Name' => '3', - 'Children' => new ArrayList(), - ] - ), - ] - ), - ] - ); - - // Make sure that including a loop inside a loop will not destroy the internal count of - // items, checked by using "Last" - $this->assertEqualIgnoringWhitespace( - '1ab23last', - $this->render( - '<% loop $Foo %>$Name<% loop Children %>$Name<% end_loop %><% if $IsLast %>last<% end_if %>' - . '<% end_loop %>', - $data - ) - ); - } - - public function testLayout() - { - $this->useTestTheme( - __DIR__ . '/SSTemplateEngineTest', - 'layouttest', - function () { - $engine = new SSTemplateEngine('Page'); - $this->assertEquals("Foo\n\n", $engine->render(new ViewLayerData([]))); - - $engine = new SSTemplateEngine(['Shortcodes', 'Page']); - $this->assertEquals("[file_link]\n\n", $engine->render(new ViewLayerData([]))); - } - ); - } - - public static function provideRenderWithSourceFileComments(): array - { - $i = __DIR__ . '/SSTemplateEngineTest/templates/Includes'; - $f = __DIR__ . '/SSTemplateEngineTest/templates/SSTemplateEngineTestComments'; - return [ - [ - 'name' => 'SSTemplateEngineTestCommentsFullSource', - 'expected' => "" - . "" - . "" - . "" - . "\t" - . "\t" - . "" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsFullSourceHTML4Doctype', - 'expected' => "" - . "" - . "" - . "" - . "\t" - . "\t" - . "" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsFullSourceNoDoctype', - 'expected' => "" - . "" - . "\t" - . "\t" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsFullSourceIfIE', - 'expected' => "" - . "" - . "" - . "" - . "" - . " " - . "\t" - . "\t" - . "" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsFullSourceIfIENoDoctype', - 'expected' => "" - . "" - . "" - . " " - . "" - . " " - . "\t" - . "\t" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsPartialSource', - 'expected' => - "" - . "" - . "", - ], - [ - 'name' => 'SSTemplateEngineTestCommentsWithInclude', - 'expected' => - "" - . "$Arg1
$Arg2
{$Arg2.Count}
diff --git a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestProcessHead.ss b/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestProcessHead.ss deleted file mode 100644 index 094cfa29a33..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestProcessHead.ss +++ /dev/null @@ -1,3 +0,0 @@ - - <% require themedJavascript(RequirementsTest_a) %> - diff --git a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestRecursiveInclude.ss b/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestRecursiveInclude.ss deleted file mode 100644 index af5b2db8a97..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestRecursiveInclude.ss +++ /dev/null @@ -1,6 +0,0 @@ -$Title -<% if Children %> -<% loop Children %> -<% include SSTemplateEngineTestRecursiveInclude %> -<% end_loop %> -<% end_if %> diff --git a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestTypePreservation.ss b/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestTypePreservation.ss deleted file mode 100644 index 0f9e2f8bf9f..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Includes/SSTemplateEngineTestTypePreservation.ss +++ /dev/null @@ -1 +0,0 @@ -$Test.Type($Argument) diff --git a/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedOnceBase.ss b/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedOnceBase.ss deleted file mode 100644 index 75700a0d6bf..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedOnceBase.ss +++ /dev/null @@ -1 +0,0 @@ -Namespace/IncludedOnceBase.ss diff --git a/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedTwice.ss b/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedTwice.ss deleted file mode 100644 index ef0b569e011..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Namespace/IncludedTwice.ss +++ /dev/null @@ -1 +0,0 @@ -Namespace/IncludedTwice.ss diff --git a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedOnceSub.ss b/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedOnceSub.ss deleted file mode 100644 index b3f7fb5b87d..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedOnceSub.ss +++ /dev/null @@ -1 +0,0 @@ -Namespace/Includes/IncludedOnceSub.ss diff --git a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedTwice.ss b/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedTwice.ss deleted file mode 100644 index aa0e4c2ba0d..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/IncludedTwice.ss +++ /dev/null @@ -1 +0,0 @@ -Namespace/Includes/IncludedTwice.ss diff --git a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/NamespaceInclude.ss b/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/NamespaceInclude.ss deleted file mode 100644 index 48d7eb546e5..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/Namespace/Includes/NamespaceInclude.ss +++ /dev/null @@ -1 +0,0 @@ -NamespaceInclude diff --git a/tests/php/View/SSTemplateEngineTest/templates/RSSFeedTest.ss b/tests/php/View/SSTemplateEngineTest/templates/RSSFeedTest.ss deleted file mode 100644 index afcae7f76ec..00000000000 --- a/tests/php/View/SSTemplateEngineTest/templates/RSSFeedTest.ss +++ /dev/null @@ -1,6 +0,0 @@ - -Nested theme page
diff --git a/tests/php/View/SSTemplateEngineTest_findTemplate/myproject/_config.php b/tests/php/View/SSTemplateEngineTest_findTemplate/myproject/_config.php deleted file mode 100644 index b3d9bbc7f37..00000000000 --- a/tests/php/View/SSTemplateEngineTest_findTemplate/myproject/_config.php +++ /dev/null @@ -1 +0,0 @@ -parent = $parent ; - $this->flags = array() ; - } - - function __set( $k, $v ) { - $this->flags[$k] = $v ; - return $v ; - } - - function __get( $k ) { - if ( isset( $this->flags[$k] ) ) return $this->flags[$k] ; - if ( isset( $this->parent ) ) return $this->parent->$k ; - return NULL ; - } -} - -/** - * PHPWriter contains several code generation snippets that are used both by the Token and the Rule compiler - */ -class PHPWriter { - - static $varid = 0 ; - - function varid() { - return '_' . (self::$varid++) ; - } - - function function_name( $str ) { - $str = preg_replace( '/-/', '_', $str ?? '' ) ; - $str = preg_replace( '/\$/', 'DLR', $str ?? '' ) ; - $str = preg_replace( '/\*/', 'STR', $str ?? '' ) ; - $str = preg_replace( '/[^\w]+/', '', $str ?? '' ) ; - return $str ; - } - - function save($id) { - return PHPBuilder::build() - ->l( - '$res'.$id.' = $result;', - '$pos'.$id.' = $this->pos;' - ); - } - - function restore( $id, $remove = FALSE ) { - $code = PHPBuilder::build() - ->l( - '$result = $res'.$id.';', - '$this->pos = $pos'.$id.';' - ); - - if ( $remove ) $code->l( - 'unset( $res'.$id.' );', - 'unset( $pos'.$id.' );' - ); - - return $code ; - } - - function match_fail_conditional( $on, $match = NULL, $fail = NULL ) { - return PHPBuilder::build() - ->b( 'if (' . $on . ')', - $match, - 'MATCH' - ) - ->b( 'else', - $fail, - 'FAIL' - ); - } - - function match_fail_block( $code ) { - $id = $this->varid() ; - - return PHPBuilder::build() - ->l( - '$'.$id.' = NULL;' - ) - ->b( 'do', - $code->replace(array( - 'MBREAK' => '$'.$id.' = TRUE; break;', - 'FBREAK' => '$'.$id.' = FALSE; break;' - )) - ) - ->l( - 'while(0);' - ) - ->b( 'if( $'.$id.' === TRUE )', 'MATCH' ) - ->b( 'if( $'.$id.' === FALSE)', 'FAIL' ) - ; - } -} - -/** - * A Token is any portion of a match rule. Tokens are responsible for generating the code to match against them. - * - * This base class provides the compile() function, which handles the token modifiers ( ? * + & ! ) - * - * Each child class should provide the function match_code() which will generate the code to match against that specific token type. - * In that generated code they should include the lines MATCH or FAIL when a match or a decisive failure occurs. These will - * be overwritten when they are injected into parent Tokens or Rules. There is no requirement on where MATCH and FAIL can occur. - * They tokens are also responsible for storing and restoring state when nessecary to handle a non-decisive failure. - * - * @author hamish - * - */ -abstract class Token extends PHPWriter { - public $optional = FALSE ; - public $zero_or_more = FALSE ; - public $one_or_more = FALSE ; - public $positive_lookahead = FALSE ; - public $negative_lookahead = FALSE ; - public $silent = FALSE ; - - public $tag = FALSE ; - - public $type ; - public $value ; - - function __construct( $type, $value = NULL ) { - $this->type = $type ; - $this->value = $value ; - } - - // abstract protected function match_code() ; - - function compile() { - $code = $this->match_code($this->value) ; - - $id = $this->varid() ; - - if ( $this->optional ) { - $code = PHPBuilder::build() - ->l( - $this->save($id), - $code->replace( array( 'FAIL' => $this->restore($id,true) )) - ); - } - - if ( $this->zero_or_more ) { - $code = PHPBuilder::build() - ->b( 'while (true)', - $this->save($id), - $code->replace( array( - 'MATCH' => NULL, - 'FAIL' => - $this->restore($id,true) - ->l( 'break;' ) - )) - ) - ->l( - 'MATCH' - ); - } - - if ( $this->one_or_more ) { - $code = PHPBuilder::build() - ->l( - '$count = 0;' - ) - ->b( 'while (true)', - $this->save($id), - $code->replace( array( - 'MATCH' => NULL, - 'FAIL' => - $this->restore($id,true) - ->l( 'break;' ) - )), - '$count += 1;' - ) - ->b( 'if ($count > 0)', 'MATCH' ) - ->b( 'else', 'FAIL' ); - } - - if ( $this->positive_lookahead ) { - $code = PHPBuilder::build() - ->l( - $this->save($id), - $code->replace( array( - 'MATCH' => - $this->restore($id) - ->l( 'MATCH' ), - 'FAIL' => - $this->restore($id) - ->l( 'FAIL' ) - ))); - } - - if ( $this->negative_lookahead ) { - $code = PHPBuilder::build() - ->l( - $this->save($id), - $code->replace( array( - 'MATCH' => - $this->restore($id) - ->l( 'FAIL' ), - 'FAIL' => - $this->restore($id) - ->l( 'MATCH' ) - ))); - } - - if ( $this->tag && !($this instanceof TokenRecurse ) ) { - $code = PHPBuilder::build() - ->l( - '$stack[] = $result; $result = $this->construct( $matchrule, "'.$this->tag.'" ); ', - $code->replace(array( - 'MATCH' => PHPBuilder::build() - ->l( - '$subres = $result; $result = array_pop($stack);', - '$this->store( $result, $subres, \''.$this->tag.'\' );', - 'MATCH' - ), - 'FAIL' => PHPBuilder::build() - ->l( - '$result = array_pop($stack);', - 'FAIL' - ) - ))); - } - - return $code ; - } - -} - -abstract class TokenTerminal extends Token { - function set_text( $text ) { - return $this->silent ? NULL : '$result["text"] .= ' . $text . ';'; - } - - protected function match_code( $value ) { - return $this->match_fail_conditional( '( $subres = $this->'.$this->type.'( '.$value.' ) ) !== FALSE', - $this->set_text('$subres') - ); - } -} - -abstract class TokenExpressionable extends TokenTerminal { - - static $expression_rx = '/ \$(\w+) | { \$(\w+) } /x'; - - function contains_expression( $value ){ - return preg_match(self::$expression_rx, $value ?? ''); - } - - function expression_replace($matches) { - return '\'.$this->expression($result, $stack, \'' . (!empty($matches[1]) ? $matches[1] : $matches[2]) . "').'"; - } - - function match_code( $value ) { - $value = preg_replace_callback(self::$expression_rx, array($this, 'expression_replace'), $value ?? ''); - return parent::match_code($value); - } -} - -class TokenLiteral extends TokenExpressionable { - function __construct( $value ) { - parent::__construct( 'literal', "'" . substr($value ?? '',1,-1) . "'" ); - } - - function match_code( $value ) { - // We inline single-character matches for speed - if ( !$this->contains_expression($value) && strlen( eval( 'return '. $value . ';' ) ) == 1 ) { - return $this->match_fail_conditional( 'substr($this->string ?? \'\',$this->pos ?? 0,1) == '.$value, - PHPBuilder::build()->l( - '$this->pos += 1;', - $this->set_text($value) - ) - ); - } - return parent::match_code($value); - } -} - -class TokenRegex extends TokenExpressionable { - static function escape( $rx ) { - $rx = str_replace( "'", "\\'", $rx ?? '' ) ; - $rx = str_replace( '\\\\', '\\\\\\\\', $rx ?? '' ) ; - return $rx ; - } - - function __construct( $value ) { - parent::__construct('rx', self::escape($value)); - } - - function match_code( $value ) { - return parent::match_code("'{$value}'"); - } -} - -class TokenWhitespace extends TokenTerminal { - function __construct( $optional ) { - parent::__construct( 'whitespace', $optional ) ; - } - - /* Call recursion indirectly */ - function match_code( $value ) { - $code = parent::match_code( '' ) ; - return $value ? $code->replace( array( 'FAIL' => NULL )) : $code ; - } -} - -class TokenRecurse extends Token { - function __construct( $value ) { - parent::__construct( 'recurse', $value ) ; - } - - function match_function( $value ) { - return "'".$this->function_name($value)."'"; - } - - function match_code( $value ) { - $function = $this->match_function($value) ; - $storetag = $this->function_name( $this->tag ? $this->tag : $this->match_function($value) ) ; - - if ( ParserCompiler::$debug ) { - $debug_header = PHPBuilder::build() - ->l( - '$indent = str_repeat( " ", $this->depth );', - '$this->depth += 2;', - '$sub = ( strlen( $this->string ) - $this->pos > 20 ) ? ( substr( $this->string, $this->pos, 20 ) . "..." ) : substr( $this->string, $this->pos );', - '$sub = preg_replace( \'/(\r|\n)+/\', " {NL} ", $sub );', - 'print( $indent."Matching against $matcher (".$sub.")\n" );' - ); - - $debug_match = PHPBuilder::build() - ->l( - 'print( $indent."MATCH\n" );', - '$this->depth -= 2;' - ); - - $debug_fail = PHPBuilder::build() - ->l( - 'print( $indent."FAIL\n" );', - '$this->depth -= 2;' - ); - } - else { - $debug_header = $debug_match = $debug_fail = NULL ; - } - - return PHPBuilder::build()->l( - '$matcher = \'match_\'.'.$function.'; $key = $matcher; $pos = $this->pos;', - $debug_header, - '$subres = ( $this->packhas( $key, $pos ) ? $this->packread( $key, $pos ) : $this->packwrite( $key, $pos, $this->$matcher(array_merge($stack, array($result))) ) );', - $this->match_fail_conditional( '$subres !== FALSE', - PHPBuilder::build()->l( - $debug_match, - $this->tag === FALSE ? - '$this->store( $result, $subres );' : - '$this->store( $result, $subres, "'.$storetag.'" );' - ), - PHPBuilder::build()->l( - $debug_fail - ) - )); - } -} - -class TokenExpressionedRecurse extends TokenRecurse { - function match_function( $value ) { - return '$this->expression($result, $stack, \''.$value.'\')'; - } -} - -class TokenSequence extends Token { - function __construct( $value ) { - parent::__construct( 'sequence', $value ) ; - } - - function match_code( $value ) { - $code = PHPBuilder::build() ; - foreach( $value as $token ) { - $code->l( - $token->compile()->replace(array( - 'MATCH' => NULL, - 'FAIL' => 'FBREAK' - )) - ); - } - $code->l( 'MBREAK' ); - - return $this->match_fail_block( $code ) ; - } -} - -class TokenOption extends Token { - function __construct( $opt1, $opt2 ) { - parent::__construct( 'option', array( $opt1, $opt2 ) ) ; - } - - function match_code( $value ) { - $id = $this->varid() ; - $code = PHPBuilder::build() - ->l( - $this->save($id) - ) ; - - foreach ( $value as $opt ) { - $code->l( - $opt->compile()->replace(array( - 'MATCH' => 'MBREAK', - 'FAIL' => NULL - )), - $this->restore($id) - ); - } - $code->l( 'FBREAK' ) ; - - return $this->match_fail_block( $code ) ; - } -} - - -/** - * Handles storing of information for an expression that applys to the next token, and deletion of that - * information after applying - * - * @author Hamish Friedlander - */ -class Pending { - function __construct() { - $this->what = NULL ; - } - - function set( $what, $val = TRUE ) { - $this->what = $what ; - $this->val = $val ; - } - - function apply_if_present( $on ) { - if ( $this->what !== NULL ) { - $what = $this->what ; - $on->$what = $this->val ; - - $this->what = NULL ; - } - } -} - -/** - * Rule parsing and code generation - * - * A rule is the basic unit of a PEG. This parses one rule, and generates a function that will match on a string - * - * @author Hamish Friedlander - */ -class Rule extends PHPWriter { - - static $rule_rx = '@ - (?
- $x = new ExampleParser( 'string to parse' ) ;
- $res = $x->match_Expr() ;
-
-
-### Parser Format
-
-Parsers are contained within a PHP file, in one or more special comment blocks that start with `/*!* [name | !pragma]` (like a docblock, but with an
-exclamation mark in the middle of the stars)
-
-You can have multiple comment blocks, all of which are treated as contiguous for the purpose of compiling. During compilation these blocks will be replaced
-with a set of "matching" functions (functions which match a string against their rules) for each rule in the block.
-
-The optional name marks the start of a new set of parser rules. This is currently unused, but might be used in future for opimization & debugging purposes.
-If unspecified, it defaults to the same name as the previous parser comment block, or 'Anonymous Parser' if no name has ever been set.
-
-If the name starts with an '!' symbol, that comment block is a pragma, and is treated not as some part of the parser, but as a special block of meta-data
-
-Lexically, these blocks are a set of rules & comments. A rule can be a base rule or an extension rule
-
-##### Base rules
-
-Base rules consist of a name for the rule, some optional arguments, the matching rule itself, and an optional set of attached functions
-
-NAME ( "(" ARGUMENT, ... ")" )? ":" MATCHING_RULE
- ATTACHED_FUNCTIONS?
-
-Names must be the characters a-z, A-Z, 0-9 and _ only, and must not start with a number
-
-Base rules can be split over multiple lines as long as subsequent lines are indented
-
-##### Extension rules
-
-Extension rules are either the same as a base rule but with an addition name of the rule to extend, or as a replacing extension consist of
-a name for the rule, the name of the rule to extend, and optionally: some arguments, some replacements, and a set of attached functions
-
-NAME extend BASENAME ( "(" ARGUMENT, ... ")" )? ":" MATCHING_RULE
- ATTACHED_FUNCTIONS?
-
-NAME extends BASENAME ( "(" ARGUMENT, ... ")" )? ( ";" REPLACE "=>" REPLACE_WITH, ... )?
- ATTACHED_FUNCTIONS?
-
-##### Tricks and traps
-
-We allow indenting a parser block, but only in a consistant manner - whatever the indent of the /*** marker becomes the "base" indent, and needs to be used
-for all lines. You can mix tabs and spaces, but the indent must always be an exact match - if the "base" indent is a tab then two spaces, every line within the
-block also needs indenting with a tab then two spaces, not two tabs (even if in your editor, that gives the same indent).
-
-Any line with more than the "base" indent is considered a continuation of the previous rule
-
-Any line with less than the "base" indent is an error
-
-This might get looser if I get around to re-writing the internal "parser parser" in php-peg, bootstrapping the whole thing
-
-### Rules
-
-PEG matching rules try to follow standard PEG format, summarised thusly:
-
-
- token* - Token is optionally repeated
- token+ - Token is repeated at least one
- token? - Token is optionally present
-
- tokena tokenb - Token tokenb follows tokena, both of which are present
- tokena | tokenb - One of tokena or tokenb are present, prefering tokena
-
- &token - Token is present next (but not consumed by parse)
- !token - Token is not present next (but not consumed by parse)
-
- ( expression ) - Grouping for priority
-
-
-But with these extensions:
-
-
- < or > - Optionally match whitespace
- [ or ] - Require some whitespace
-
-
-### Tokens
-
-Tokens may be
-
- - bare-words, which are recursive matchers - references to token rules defined elsewhere in the grammar,
- - literals, surrounded by `"` or `'` quote pairs. No escaping support is provided in literals.
- - regexs, surrounded by `/` pairs.
- - expressions - single words (match \w+) starting with `$` or more complex surrounded by `${ }` which call a user defined function to perform the match
-
-##### Regular expression tokens
-
-Automatically anchored to the current string start - do not include a string start anchor (`^`) anywhere. Always acts as when the 'x' flag is enabled in PHP -
-whitespace is ignored unless escaped, and '#' stats a comment.
-
-Be careful when ending a regular expression token - the '*/' pattern (as in /foo\s*/) will end a PHP comment. Since the 'x' flag is always active,
-just split with a space (as in / foo \s* /)
-
-### Expressions
-
-Expressions allow run-time calculated matching. You can embed an expression within a literal or regex token to
-match against a calculated value, or simply specify the expression as a token to match against a dynamic rule.
-
-#### Expression stack
-
-When getting a value to use for an expression, the parser will travel up the stack looking for a set value. The expression
-stack is a list of all the rules passed through to get to this point. For example, given the parser
-
-
- A: $a
- B: A
- C: B
-
-
-The expression stack for finding $a will be C, B, A - in other words, the A rule will be checked first, followed by B, followed by C
-
-#### In terminals (literals and regexes)
-
-The token will be replaced by the looked up value. To find the value for the token, the expression stack will be
-travelled up checking for one of the following:
-
- - A key / value pair in the result array node
- - A rule-attached method INCLUDING `$` ( i.e. `function $foo()` )
-
-If no value is found it will then check if a method or a property excluding the $ exists on the parser. If neither of those is found
-the expression will be replaced with an exmpty string/
-
-#### As tokens
-
-The token will be looked up to find a value, which must be the name of a matching rule. That rule will then be matched
-against as if the token was a recurse token for that rule.
-
-To find the name of the rule to match against, the expression stack will be travelled up checking for one of the following:
-
- - A key / value pair in the result array node
- - A rule-attached method INCLUDING `$` ( i.e. `function $foo()` )
-
-If no value is found it will then check if a method or a property excluding the $ exists on the parser. If neither of those if found
-the rule will fail to match.
-
-#### Tricks and traps
-
-Be careful against using a token expression when you meant to use a terminal expression
-
-
- quoted_good: q:/['"]/ string "$q"
- quoted_bad: q:/['"]/ string $q
-
-
-`"$q"` matches against the value of q again. `$q` tries to match against a rule named `"` or `'` (both of which are illegal rule
-names, and will therefore fail)
-
-### Named matching rules
-
-Tokens and groups can be given names by prepending name and `:`, e.g.,
-
-
- rulea: "'" name:( tokena tokenb )* "'"
-
-
-There must be no space betweeen the name and the `:`
-
-
- badrule: "'" name : ( tokena tokenb )* "'"
-
-
-Recursive matchers can be given a name the same as their rule name by prepending with just a `:`. These next two rules are equivilent
-
-
- rulea: tokena tokenb:tokenb
- rulea: tokena :tokenb
-
-
-### Rule-attached functions
-
-Each rule can have a set of functions attached to it. These functions can be defined
-
-- in-grammar by indenting the function body after the rule
-- in-class after close of grammar comment by defining a regular method who's name is `{$rulename}_{$functionname}`, or `{$rulename}{$functionname}` if function name starts with `_`
-- in a sub class
-
-All functions that are not in-grammar must have PHP compatible names (see PHP name mapping). In-grammar functions will have their names converted if needed.
-
-All these definitions define the same rule-attached function
-
-
- class A extends Parser {
- /*!* Parser
- foo: bar baz
- function bar() {}
- */
-
- function foo_bar() {}
- }
-
- class B extends A {
- function foo_bar() {}
- }
-
-
-### PHP name mapping
-
-Rules in the grammar map to php functions named `match_{$rulename}`. However rule names can contain characters that php functions can't.
-These characters are remapped:
-
-
- '-' => '_'
- '$' => 'DLR'
- '*' => 'STR'
-
-
-Other dis-allowed characters are removed.
-
-## Results
-
-Results are a tree of nested arrays.
-
-Without any specific control, each rules result will just be the text it matched against in a `['text']` member. This member must always exist.
-
-Marking a subexpression, literal, regex or recursive match with a name (see Named matching rules) will insert a member into the
-result array named that name. If there is only one match it will be a single result array. If there is more than one match it will be an array of arrays.
-
-You can override result storing by specifying a rule-attached function with the given name. It will be called with a reference to the current result array
-and the sub-match - in this case the default storage action will not occur.
-
-If you specify a rule-attached function for a recursive match, you do not need to name that token at all - it will be call automatically. E.g.
-
-
- rulea: tokena tokenb
- function tokenb ( &$res, $sub ) { print 'Will be called, even though tokenb is not named or marked with a :' ; }
-
-
-You can also specify a rule-attached function called `*`, which will be called with every recursive match made
-
-
- rulea: tokena tokenb
- function * ( &$res, $sub ) { print 'Will be called for both tokena and tokenb' ; }
-
-
-### Silent matches
-
-By default all matches are added to the 'text' property of a result. By prepending a member with `.` that match will not be added to the ['text'] member. This
-doesn't affect the other result properties that named rules' add.
-
-### Inheritance
-
-Rules can inherit off other rules using the keyword extends. There are several ways to change the matching of the rule, but
-they all share a common feature - when building a result set the rule will also check the inherited-from rule's rule-attached
-functions for storage handlers. This lets you do something like
-
-
-A: Foo Bar Baz
- function *(){ /* Generic store handler */ }
-
-B extends A
- function Bar(){ /* Custom handling for Bar - Foo and Baz will still fall through to the A#* function defined above */ }
-
-
-The actual matching rule can be specified in three ways:
-
-#### Duplication
-
-If you don't specify a new rule or a replacement set the matching rule is copied as is. This is useful when you want to
-override some storage logic but not the rule itself
-
-#### Text replacement
-
-You can replace some parts of the inherited rule using test replacement by using a ';' instead of an ':' after the name
- of the extended rule. You can then put replacements in a comma seperated list. An example might help
-
-
-A: Foo | Bar | Baz
-
-# Makes B the equivalent of Foo | Bar | (Baz | Qux)
-B extends A: Baz => (Baz | Qux)
-
-
-Note that the replacements are not quoted. The exception is when you want to replace with the empty string, e.g.
-
-
-A: Foo | Bar | Baz
-
-# Makes B the equivalent of Foo | Bar
-B extends A: | Baz => ""
-
-
-Currently there is no escaping supported - if you want to replace "," or "=>" characters you'll have to use full replacement
-
-#### Full replacement
-
-You can specify an entirely new rule in the same format as a non-inheriting rule, eg.
-
-
-A: Foo | Bar | Baz
-
-B extends A: Foo | Bar | (Baz Qux)
-
-
-This is useful is the rule changes too much for text replacement to be readable, but want to keep the storage logic
-
-### Pragmas
-
-When opening a parser comment block, if instead of a name (or no name) you put a word starting with '!', that comment block is treated as a pragma - not
-part of the parser language itself, but some other instruction to the compiler. These pragmas are currently understood:
-
- !silent
-
- This is a comment that should only appear in the source code. Don't output it in the generated code
-
- !insert_autogen_warning
-
- Insert a warning comment into the generated code at this point, warning that the file is autogenerated and not to edit it
-
-## TODO
-
-- Allow configuration of whitespace - specify what matches, and wether it should be injected into results as-is, collapsed, or not at all
-- Allow inline-ing of rules into other rules for speed
-- More optimisation
-- Make Parser-parser be self-generated, instead of a bad hand rolled parser like it is now.
-- PHP token parser, and other token streams, instead of strings only like now
diff --git a/thirdparty/php-peg/cli.php b/thirdparty/php-peg/cli.php
deleted file mode 100644
index ab979615d45..00000000000
--- a/thirdparty/php-peg/cli.php
+++ /dev/null
@@ -1,5 +0,0 @@
-