From 71892085fc49c6aad0e940095e7dbec4c02273a8 Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Fri, 18 Feb 2011 17:06:11 +1300 Subject: [PATCH] ENHANCEMENT: Add Up support --- core/SSTemplateParser.php | 64 ++++++++++------ core/SSTemplateParser.php.inc | 64 ++++++++++------ core/SSViewer.php | 135 ++++++++++++++++++++++++++++++++-- tests/SSViewerTest.php | 131 +++++++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+), 48 deletions(-) diff --git a/core/SSTemplateParser.php b/core/SSTemplateParser.php index 3e9b7a0d3..0094a21eb 100644 --- a/core/SSTemplateParser.php +++ b/core/SSTemplateParser.php @@ -67,12 +67,6 @@ class SSTemplateParser extends Parser { */ protected $includeDebuggingComments = false; - function construct($name) { - $result = parent::construct($name); - $result['tags'] = array(); - return $result; - } - /* Word: / [A-Za-z_] [A-Za-z0-9_]* / */ function match_Word ($substack = array()) { $result = array("name"=>"Word", "text"=>""); @@ -169,7 +163,7 @@ class SSTemplateParser extends Parser { if (isset($res['php'])) $res['php'] .= ', '; else $res['php'] = ''; - $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : $sub['php']; + $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : str_replace('$$FINAL', 'XML_val', $sub['php']); } /* Call: Method:Word ( "(" < :CallArguments? > ")" )? */ @@ -354,7 +348,7 @@ class SSTemplateParser extends Parser { function Lookup__construct(&$res) { - $res['php'] = '$item'; + $res['php'] = '$scope'; $res['LookupSteps'] = array(); } @@ -381,7 +375,7 @@ class SSTemplateParser extends Parser { } function Lookup_LastLookupStep(&$res, $sub) { - $this->Lookup_AddLookupStep($res, $sub, 'XML_val'); + $this->Lookup_AddLookupStep($res, $sub, '$$FINAL'); } /* SimpleInjection: '$' :Lookup */ @@ -474,7 +468,7 @@ class SSTemplateParser extends Parser { function Injection_STR(&$res, $sub) { - $res['php'] = '$val .= '. $sub['Lookup']['php'] . ';'; + $res['php'] = '$val .= '. str_replace('$$FINAL', 'XML_val', $sub['Lookup']['php']) . ';'; } /* DollarMarkedLookup: SimpleInjection */ @@ -777,11 +771,11 @@ class SSTemplateParser extends Parser { 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 $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']); } else { if (!isset($res['php'])) $res['php'] = ''; - $res['php'] .= $sub['php']; + $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } } @@ -811,7 +805,7 @@ class SSTemplateParser extends Parser { $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); + $res['php'] = str_replace('$$FINAL', 'hasValue', $php); } } @@ -1519,9 +1513,9 @@ class SSTemplateParser extends Parser { } /** - * This is an example of a block handler function. This one handles the control tag. + * This is an example of a block handler function. This one handles the loop tag. */ - function ClosedBlock_Handle_Control(&$res) { + function ClosedBlock_Handle_Loop(&$res) { if ($res['ArgumentCount'] != 1) { throw new SSTemplateParseException('Either no or too many arguments in control block. Must be one argument only.', $this); } @@ -1531,11 +1525,39 @@ class SSTemplateParser extends Parser { 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']); + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); return - 'array_push($itemStack, $item); if($loop = '.$on.') foreach($loop as $key => $item) {' . PHP_EOL . + $on . '; $scope->pushScope(); while (($key = $scope->next()) !== false) {' . PHP_EOL . $res['Template']['php'] . PHP_EOL . - '} $item = array_pop($itemStack); '; + '}; $scope->popScope(); '; + } + + /** + * The deprecated closed block handler for control blocks + * @deprecated + */ + function ClosedBlock_Handle_Control(&$res) { + return $this->ClosedBlock_Handle_Loop($res); + } + + /** + * The closed block handler for with blocks + */ + function ClosedBlock_Handle_With(&$res) { + if ($res['ArgumentCount'] != 1) { + throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one argument only.', $this); + } + + $arg = $res['Arguments'][0]; + if ($arg['ArgumentMode'] == 'string') { + throw new SSTemplateParseException('Control block cant take string as argument.', $this); + } + + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); + return + $on . '; $scope->pushScope();' . PHP_EOL . + $res['Template']['php'] . PHP_EOL . + '; $scope->popScope(); '; } /* OpenBlock: '<%' < !NotBlockTag BlockName:Word ( [ :BlockArguments ] )? > '%>' */ @@ -1642,12 +1664,12 @@ class SSTemplateParser extends Parser { if($this->includeDebuggingComments) { // Add include filename comments on dev sites return '$val .= \'\';'. "\n". - '$val .= SSViewer::parse_template('.$php.', $item);'. "\n". + '$val .= SSViewer::parse_template('.$php.', $scope->getItem());'. "\n". '$val .= \'\';'. "\n"; } else { return - '$val .= SSViewer::execute_template('.$php.', $item);'. "\n"; + '$val .= SSViewer::execute_template('.$php.', $scope->getItem());'. "\n"; } } @@ -1655,7 +1677,7 @@ class SSTemplateParser extends Parser { * This is an open block handler, for the <% debug %> utility tag */ function OpenBlock_Handle_Debug(&$res) { - if ($res['ArgumentCount'] == 0) return 'Debug::show($item);'; + if ($res['ArgumentCount'] == 0) return '$scope->debug();'; else if ($res['ArgumentCount'] == 1) { $arg = $res['Arguments'][0]; diff --git a/core/SSTemplateParser.php.inc b/core/SSTemplateParser.php.inc index 8c1abb4e9..2f9dbb14d 100644 --- a/core/SSTemplateParser.php.inc +++ b/core/SSTemplateParser.php.inc @@ -81,12 +81,6 @@ 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_]* / @@ -107,7 +101,7 @@ class SSTemplateParser extends Parser { if (isset($res['php'])) $res['php'] .= ', '; else $res['php'] = ''; - $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : $sub['php']; + $res['php'] .= ($sub['ArgumentMode'] == 'default') ? $sub['string_php'] : str_replace('$$FINAL', 'XML_val', $sub['php']); } /*!* @@ -128,7 +122,7 @@ class SSTemplateParser extends Parser { */ function Lookup__construct(&$res) { - $res['php'] = '$item'; + $res['php'] = '$scope'; $res['LookupSteps'] = array(); } @@ -155,7 +149,7 @@ class SSTemplateParser extends Parser { } function Lookup_LastLookupStep(&$res, $sub) { - $this->Lookup_AddLookupStep($res, $sub, 'XML_val'); + $this->Lookup_AddLookupStep($res, $sub, '$$FINAL'); } /*!* @@ -168,7 +162,7 @@ class SSTemplateParser extends Parser { Injection: BracketInjection | SimpleInjection */ function Injection_STR(&$res, $sub) { - $res['php'] = '$val .= '. $sub['Lookup']['php'] . ';'; + $res['php'] = '$val .= '. str_replace('$$FINAL', 'XML_val', $sub['Lookup']['php']) . ';'; } /*!* @@ -261,11 +255,11 @@ class SSTemplateParser extends Parser { 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 $res['php'] = str_replace('$$FINAL', 'XML_val', $sub['lookup_php']); } else { if (!isset($res['php'])) $res['php'] = ''; - $res['php'] .= $sub['php']; + $res['php'] .= str_replace('$$FINAL', 'XML_val', $sub['php']); } } @@ -289,7 +283,7 @@ class SSTemplateParser extends Parser { $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); + $res['php'] = str_replace('$$FINAL', 'hasValue', $php); } } @@ -429,9 +423,9 @@ class SSTemplateParser extends Parser { } /** - * This is an example of a block handler function. This one handles the control tag. + * This is an example of a block handler function. This one handles the loop tag. */ - function ClosedBlock_Handle_Control(&$res) { + function ClosedBlock_Handle_Loop(&$res) { if ($res['ArgumentCount'] != 1) { throw new SSTemplateParseException('Either no or too many arguments in control block. Must be one argument only.', $this); } @@ -441,11 +435,39 @@ class SSTemplateParser extends Parser { 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']); + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); return - 'array_push($itemStack, $item); if($loop = '.$on.') foreach($loop as $key => $item) {' . PHP_EOL . + $on . '; $scope->pushScope(); while (($key = $scope->next()) !== false) {' . PHP_EOL . $res['Template']['php'] . PHP_EOL . - '} $item = array_pop($itemStack); '; + '}; $scope->popScope(); '; + } + + /** + * The deprecated closed block handler for control blocks + * @deprecated + */ + function ClosedBlock_Handle_Control(&$res) { + return $this->ClosedBlock_Handle_Loop($res); + } + + /** + * The closed block handler for with blocks + */ + function ClosedBlock_Handle_With(&$res) { + if ($res['ArgumentCount'] != 1) { + throw new SSTemplateParseException('Either no or too many arguments in with block. Must be one argument only.', $this); + } + + $arg = $res['Arguments'][0]; + if ($arg['ArgumentMode'] == 'string') { + throw new SSTemplateParseException('Control block cant take string as argument.', $this); + } + + $on = str_replace('$$FINAL', 'obj', ($arg['ArgumentMode'] == 'default') ? $arg['lookup_php'] : $arg['php']); + return + $on . '; $scope->pushScope();' . PHP_EOL . + $res['Template']['php'] . PHP_EOL . + '; $scope->popScope(); '; } /*!* @@ -492,12 +514,12 @@ class SSTemplateParser extends Parser { if($this->includeDebuggingComments) { // Add include filename comments on dev sites return '$val .= \'\';'. "\n". - '$val .= SSViewer::parse_template('.$php.', $item);'. "\n". + '$val .= SSViewer::parse_template('.$php.', $scope->getItem());'. "\n". '$val .= \'\';'. "\n"; } else { return - '$val .= SSViewer::execute_template('.$php.', $item);'. "\n"; + '$val .= SSViewer::execute_template('.$php.', $scope->getItem());'. "\n"; } } @@ -505,7 +527,7 @@ class SSTemplateParser extends Parser { * This is an open block handler, for the <% debug %> utility tag */ function OpenBlock_Handle_Debug(&$res) { - if ($res['ArgumentCount'] == 0) return 'Debug::show($item);'; + if ($res['ArgumentCount'] == 0) return '$scope->debug();'; else if ($res['ArgumentCount'] == 1) { $arg = $res['Arguments'][0]; diff --git a/core/SSViewer.php b/core/SSViewer.php index 56dcbe00a..379e896bc 100755 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -1,4 +1,129 @@ item = $item; + $this->localIndex=0; + $this->itemStack[] = array($this->item, null, null, null, 0); + } + + function getItem(){ + return $this->itemIterator ? $this->itemIterator->current() : $this->item; + } + + function resetLocalScope(){ + list($this->item, $this->itemIterator, $this->popIndex, $this->upIndex, $this->currentIndex) = $this->itemStack[$this->localIndex]; + array_splice($this->itemStack, $this->localIndex+1); + } + + function obj($name){ + + switch ($name) { + case 'Up': + list($this->item, $this->itemIterator, $unused2, $this->upIndex, $this->currentIndex) = $this->itemStack[$this->upIndex]; + break; + + case 'Top': + list($this->item, $this->itemIterator, $unused2, $this->upIndex, $this->currentIndex) = $this->itemStack[0]; + break; + + default: + $on = $this->itemIterator ? $this->itemIterator->current() : $this->item; + + $this->item = call_user_func_array(array($on, 'obj'), func_get_args()); + $this->itemIterator = null; + $this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack)-1; + $this->currentIndex = count($this->itemStack); + break; + } + + $this->itemStack[] = array($this->item, $this->itemIterator, null, $this->upIndex, $this->currentIndex); + return $this; + } + + function pushScope(){ + $newLocalIndex = count($this->itemStack)-1; + + $this->popIndex = $this->itemStack[$newLocalIndex][2] = $this->localIndex; + $this->localIndex = $newLocalIndex; + + // We normally keep any previous itemIterator around, so local $Up calls reference the right element. But + // once we enter a new global scope, we need to make sure we use a new one + $this->itemIterator = $this->itemStack[$newLocalIndex][1] = null; + + return $this; + } + + function popScope(){ + $this->localIndex = $this->popIndex; + $this->resetLocalScope(); + + return $this; + } + + function next(){ + if (!$this->item) return false; + + if (!$this->itemIterator) { + if (is_array($this->item)) $this->itemIterator = new ArrayIterator($this->item); + else $this->itemIterator = $this->item->getIterator(); + + $this->itemStack[$this->localIndex][1] = $this->itemIterator; + $this->itemIterator->rewind(); + } + else { + $this->itemIterator->next(); + } + + $this->resetLocalScope(); + + if (!$this->itemIterator->valid()) return false; + return $this->itemIterator->key(); + } + + function __call($name, $arguments) { + $on = $this->itemIterator ? $this->itemIterator->current() : $this->item; + $retval = call_user_func_array(array($on, $name), $arguments); + + $this->resetLocalScope(); + return $retval; + } +} + /** * Parses a template file with an *.ss file extension. * @@ -422,14 +547,12 @@ class SSViewer { } } - $itemStack = array(); - $val = ""; - $valStack = array(); + $scope = new SSViewer_DataPresenter($item); + $val = ""; $valStack = array(); include($cacheFile); - $output = $val; - $output = Requirements::includeInHTML($template, $output); + $output = Requirements::includeInHTML($template, $val); array_pop(SSViewer::$topLevel); @@ -528,7 +651,7 @@ class SSViewer_FromString extends SSViewer { echo ""; } - $itemStack = array(); + $scope = new SSViewer_DataPresenter($item); $val = ""; $valStack = array(); diff --git a/tests/SSViewerTest.php b/tests/SSViewerTest.php index 61cd8cee3..874d50dd0 100644 --- a/tests/SSViewerTest.php +++ b/tests/SSViewerTest.php @@ -329,6 +329,137 @@ after') $this->assertEquals('A A1 A1 i A1 ii A2 A3', $rationalisedResult); } + + function assertEqualIgnoringWhitespace($a, $b) { + $this->assertEquals(preg_replace('/\s+/', '', $a), preg_replace('/\s+/', '', $b)); + } + + /** + * Test $Up works when the scope $Up refers to was entered with a "with" block + */ + function testUpInWith() { + + // Data to run the loop tests on - three levels deep + $data = new ArrayData(array( + 'Name' => 'Top', + 'Foo' => new ArrayData(array( + 'Name' => 'Foo', + 'Bar' => new ArrayData(array( + 'Name' => 'Bar', + 'Baz' => new ArrayData(array( + 'Name' => 'Baz' + )), + 'Qux' => new ArrayData(array( + '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('BarFoo', + $this->render('<% with Foo.Bar %>{$Name}{$Up.Name}<% end_with %>', $data)); + + // Stepping up & back down the scope tree + $this->assertEquals('BazBarQux', + $this->render('<% with Foo.Bar.Baz %>{$Name}{$Up.Name}{$Up.Qux.Name}<% end_with %>', $data)); + + // Using $Up in a with block + $this->assertEquals('BazBarQux', + $this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}{$Qux.Name}<% end_with %><% end_with %>', $data)); + + // Stepping up & back down the scope tree with with blocks + $this->assertEquals('BazBarQuxBarBaz', + $this->render('<% with Foo.Bar.Baz %>{$Name}<% with $Up %>{$Name}<% with Qux %>{$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.Bar.Baz %><% with Up %><% with Qux %>{$Up.Up.Name}<% end_with %><% end_with %><% end_with %>', $data)); + + // Using $Up.Up, where first $Up points to an Up used in a local scope lookup, should still skip to Foo + $this->assertEquals('Foo', + $this->render('<% with Foo.Bar.Baz.Up.Qux %>{$Up.Up.Name}<% end_with %>', $data)); + } + + /** + * Test $Up works when the scope $Up refers to was entered with a "loop" block + */ + function testUpInLoop(){ + + // Data to run the loop tests on - one sequence of three items, each with a subitem + $data = new ArrayData(array( + 'Name' => 'Top', + 'Foo' => new DataObjectSet(array( + new ArrayData(array( + 'Name' => '1', + 'Sub' => new ArrayData(array( + 'Name' => 'Bar' + )) + )), + new ArrayData(array( + 'Name' => '2', + 'Sub' => new ArrayData(array( + 'Name' => 'Baz' + )) + )), + new ArrayData(array( + 'Name' => '3', + 'Sub' => new ArrayData(array( + '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 + ) + ); + } } /**