├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── examples ├── 001.css ├── cssZenGarden.html └── simple.php ├── library └── Respect │ └── Template │ ├── Adapter.php │ ├── Adapters │ ├── A.php │ ├── AbstractAdapter.php │ ├── Dom.php │ ├── HtmlElement.php │ ├── String.php │ └── Traversable.php │ ├── Decorators │ ├── AbstractDecorator.php │ ├── Append.php │ ├── Clean.php │ ├── CleanAppend.php │ └── Replace.php │ ├── Document.php │ ├── Html.php │ ├── HtmlElement.php │ └── Query.php ├── phpunit.xml.dist └── tests └── library └── Respect └── Template ├── AdapterTest.php ├── Adapters ├── ATest.php ├── HtmlElementTest.php ├── StringTest.php └── TraversableTest.php ├── Decorators ├── AppendTest.php ├── CleanAppendTest.php ├── CleanTest.php └── ReplaceTest.php ├── HtmlElementTest.php └── HtmlTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | phpunit.xml 3 | vendor/ 4 | *.lock 5 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - tests/* 4 | 5 | checks: 6 | php: 7 | code_rating: true 8 | 9 | tools: 10 | external_code_coverage: true 11 | php_analyzer: true 12 | php_changetracking: true 13 | php_code_sniffer: 14 | config: 15 | standard: "PSR2" 16 | php_cpd: true 17 | php_mess_detector: true 18 | php_pdepend: true 19 | sensiolabs_security_checker: true 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: 2 | false 3 | 4 | language: 5 | php 6 | 7 | php: 8 | - 5.3 9 | - 5.4 10 | - 5.5 11 | - 5.6 12 | - hhvm 13 | - hhvm-nightly 14 | 15 | cache: 16 | directories: 17 | - vendor 18 | 19 | before_script: 20 | - composer install --dev --no-interaction --prefer-source 21 | 22 | script: 23 | - vendor/bin/phpunit --configuration phpunit.xml.dist --colors --coverage-clover=coverage.clover 24 | 25 | after_script: 26 | - wget https://scrutinizer-ci.com/ocular.phar 27 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 28 | 29 | matrix: 30 | allow_failures: 31 | - php: hhvm 32 | - php: hhvm-nightly 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Respect\Template 2 | 3 | Contributions to Respect\Template are always welcome. You make our lives easier by 4 | sending us your contributions through [GitHub pull requests](http://help.github.com/pull-requests). 5 | 6 | Pull requests for bug fixes must be based on the current stable branch whereas 7 | pull requests for new features must be based on `master`. 8 | 9 | Due to time constraints, we are not always able to respond as quickly as we 10 | would like. Please do not take delays personal and feel free to remind us here, 11 | on IRC, or on Gitter if you feel that we forgot to respond. 12 | 13 | ## Using Respect\Template From a Git Checkout 14 | 15 | The following commands can be used to perform the initial checkout of Respect\Template: 16 | 17 | ```shell 18 | git clone git://github.com/Respect/Template.git 19 | cd Template 20 | ``` 21 | 22 | Retrieve Respect\Template's dependencies using [Composer](http://getcomposer.org/): 23 | 24 | ```shell 25 | composer install 26 | ``` 27 | 28 | ## Running Tests 29 | 30 | After run `composer install` on the library's root directory you must run PHPUnit. 31 | 32 | ### Linux 33 | 34 | You can test the project using the commands: 35 | ```shell 36 | $ vendor/bin/phpunit 37 | ``` 38 | 39 | ### Windows 40 | 41 | You can test the project using the commands: 42 | ```shell 43 | > vendor\bin\phpunit 44 | ``` 45 | 46 | No test should fail. 47 | 48 | You can tweak the PHPUnit's settings by copying `phpunit.xml.dist` to `phpunit.xml` 49 | and changing it according to your needs. 50 | 51 | ## Standards 52 | 53 | We are trying to follow the [PHP-FIG](http://www.php-fig.org)'s standards, so 54 | when you send us a pull request, be sure you are following them. 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2015, Alexandre Gomes Gaigalas. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Alexandre Gomes Gaigalas nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | Copyright (c) 2005-2011, Zend Technologies USA, Inc. 30 | All rights reserved. 31 | 32 | Redistribution and use in source and binary forms, with or without modification, 33 | are permitted provided that the following conditions are met: 34 | 35 | * Redistributions of source code must retain the above copyright notice, 36 | this list of conditions and the following disclaimer. 37 | 38 | * Redistributions in binary form must reproduce the above copyright notice, 39 | this list of conditions and the following disclaimer in the documentation 40 | and/or other materials provided with the distribution. 41 | 42 | * Neither the name of Zend Technologies USA, Inc. nor the names of its 43 | contributors may be used to endorse or promote products derived from this 44 | software without specific prior written permission. 45 | 46 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 47 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 48 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 49 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 50 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 51 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 52 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 53 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 54 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 55 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Respect\Template 2 | [![Build Status](https://img.shields.io/travis/Respect/Template.svg?style=flat-square)](http://travis-ci.org/Respect/Template) 3 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Respect/Template.svg?style=flat-square)](https://scrutinizer-ci.com/g/Respect/Template/?branch=master) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/respect/template.svg?style=flat-square)](https://packagist.org/packages/respect/template) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/respect/template.svg?style=flat-square)](https://packagist.org/packages/respect/template) 6 | [![License](https://img.shields.io/packagist/l/respect/template.svg?style=flat-square)](https://packagist.org/packages/respect/template) 7 | 8 | Pure HTML Templates. 9 | 10 | ## Installation 11 | 12 | The package is available on [Packagist](https://packagist.org/packages/respect/template). 13 | You can install it using [Composer](http://getcomposer.org). 14 | 15 | ```bash 16 | composer require respect/template 17 | ``` 18 | 19 | ## License Information 20 | 21 | See [LICENSE](LICENSE) file. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "respect/template", 3 | "description": "Pure HTML templates", 4 | "type": "library", 5 | "homepage": "http://respect.li/", 6 | "license": "BSD Style", 7 | "authors": [ 8 | { 9 | "name": "Respect/Template Contributors", 10 | "homepage": "https://github.com/Respect/Template/graphs/contributors" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3.3", 15 | "zendframework/zend-dom": "~2.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "~3.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Respect\\": "library/Respect/" 23 | } 24 | }, 25 | "extra": { 26 | "branch-alias": { 27 | "dev-master": "0.3-dev" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/001.css: -------------------------------------------------------------------------------- 1 | /* css Zen Garden default style - 'Tranquille' by Dave Shea - http://www.mezzoblue.com/ */ 2 | /* css released under Creative Commons License - http://creativecommons.org/licenses/by-nc-sa/1.0/ */ 3 | /* All associated graphics copyright 2003, Dave Shea */ 4 | /* Added: May 7th, 2003 */ 5 | 6 | 7 | /* IMPORTANT */ 8 | /* This design is not a template. You may not reproduce it elsewhere without the 9 | designer's written permission. However, feel free to study the CSS and use 10 | techniques you learn from it elsewhere. */ 11 | 12 | 13 | /* The Zen Garden default was the first I put together, and almost didn't make the cut. I briefly flirted with using 14 | 'Salmon Cream Cheese' as the main style for the Garden, but switched back to this one before launch. 15 | 16 | All graphics in this design were illustrated by me in Photoshop. Google Image Search provided inspiration for 17 | some of the elements. I did a bit of research on Kanji to come up with the characters on the top left. Anyone who 18 | can read that will most likely tell you it makes no sense, but the best I could do was putting together the 19 | characters for 'beginning' 'complete' and 'skill' to roughly say something like 'we're breaking fresh ground.' 20 | 21 | It's a stretch. */ 22 | 23 | 24 | /* basic elements */ 25 | html { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | body { 30 | font: 75% georgia, sans-serif; 31 | line-height: 1.88889; 32 | color: #555753; 33 | background: #fff url(/001/blossoms.jpg) no-repeat bottom right; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | p { 38 | margin-top: 0; 39 | text-align: justify; 40 | } 41 | h3 { 42 | font: italic normal 1.4em georgia, sans-serif; 43 | letter-spacing: 1px; 44 | margin-bottom: 0; 45 | color: #7D775C; 46 | } 47 | a:link { 48 | font-weight: bold; 49 | text-decoration: none; 50 | color: #B7A5DF; 51 | } 52 | a:visited { 53 | font-weight: bold; 54 | text-decoration: none; 55 | color: #D4CDDC; 56 | } 57 | a:hover, a:active { 58 | text-decoration: underline; 59 | color: #9685BA; 60 | } 61 | acronym { 62 | border-bottom: none; 63 | } 64 | 65 | 66 | /* specific divs */ 67 | #container { 68 | background: url(/001/zen-bg.jpg) no-repeat top left; 69 | padding: 0 175px 0 110px; 70 | margin: 0; 71 | position: relative; 72 | } 73 | 74 | #intro { 75 | min-width: 470px; 76 | width: 100%; 77 | } 78 | 79 | /* using an image to replace text in an h1. This trick courtesy Douglas Bowman, http://www.stopdesign.com/articles/css/replace-text/ */ 80 | #pageHeader h1 { 81 | background: transparent url(/001/h1.gif) no-repeat top left; 82 | margin-top: 10px; 83 | display: block; 84 | width: 219px; 85 | height: 87px; 86 | float: left; 87 | } 88 | #pageHeader h1 span { 89 | display:none 90 | } 91 | #pageHeader h2 { 92 | background: transparent url(/001/h2.gif) no-repeat top left; 93 | margin-top: 58px; 94 | margin-bottom: 40px; 95 | width: 200px; 96 | height: 18px; 97 | float: right; 98 | } 99 | #pageHeader h2 span { 100 | display:none 101 | } 102 | #pageHeader { 103 | padding-top: 20px; 104 | height: 87px; 105 | } 106 | 107 | #quickSummary { 108 | clear: both; 109 | margin: 20px 20px 20px 10px; 110 | width: 160px; 111 | float: left; 112 | } 113 | #quickSummary p { 114 | font: italic 1.1em/2.2 georgia; 115 | text-align: center; 116 | } 117 | 118 | #preamble { 119 | clear: right; 120 | padding: 0px 10px 0 10px; 121 | } 122 | #supportingText { 123 | padding-left: 10px; 124 | margin-bottom: 40px; 125 | } 126 | 127 | #footer { 128 | text-align: center; 129 | } 130 | #footer a:link, #footer a:visited { 131 | margin-right: 20px; 132 | } 133 | 134 | #linkList { 135 | margin-left: 600px; 136 | position: absolute; 137 | top: 0; 138 | right: 0; 139 | } 140 | #linkList2 { 141 | font: 10px verdana, sans-serif; 142 | background: transparent url(/001/paper-bg.jpg) top left repeat-y; 143 | padding: 10px; 144 | margin-top: 150px; 145 | width: 130px; 146 | } 147 | #linkList h3.select { 148 | background: transparent url(/001/h3.gif) no-repeat top left; 149 | margin: 10px 0 5px 0; 150 | width: 97px; 151 | height: 16px; 152 | } 153 | #linkList h3.select span { 154 | display:none 155 | } 156 | #linkList h3.favorites { 157 | background: transparent url(/001/h4.gif) no-repeat top left; 158 | margin: 25px 0 5px 0; 159 | width: 60px; 160 | height: 18px; 161 | } 162 | #linkList h3.favorites span { 163 | display:none 164 | } 165 | #linkList h3.archives { 166 | background: transparent url(/001/h5.gif) no-repeat top left; 167 | margin: 25px 0 5px 0; 168 | width:57px; 169 | height: 14px; 170 | } 171 | #linkList h3.archives span { 172 | display:none 173 | } 174 | #linkList h3.resources { 175 | background: transparent url(/001/h6.gif) no-repeat top left; 176 | margin: 25px 0 5px 0; 177 | width:63px; 178 | height: 10px; 179 | } 180 | #linkList h3.resources span { 181 | display:none 182 | } 183 | 184 | 185 | #linkList ul { 186 | margin: 0; 187 | padding: 0; 188 | } 189 | #linkList li { 190 | line-height: 1.3em; 191 | background: transparent url(/001/cr1.gif) no-repeat top center; 192 | display: block; 193 | padding-top: 5px; 194 | margin-bottom: 5px; 195 | list-style-type: none; 196 | } 197 | #linkList li a:link { 198 | color: #988F5E; 199 | } 200 | #linkList li a:visited { 201 | color: #B3AE94; 202 | } 203 | 204 | 205 | #extraDiv1 { 206 | background: transparent url(/001/cr2.gif) top left no-repeat; 207 | position: absolute; 208 | top: 40px; 209 | right: 0; 210 | width: 148px; 211 | height: 110px; 212 | } 213 | .accesskey { 214 | text-decoration: underline; 215 | } -------------------------------------------------------------------------------- /examples/cssZenGarden.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | css Zen Garden: The Beauty in CSS Design 9 | 10 | 13 | 14 | 15 | 27 | 28 | 29 | 30 |
31 |
32 | 36 | 37 |
38 |

A demonstration of what can be accomplished visually through CSS-based design. Select any style sheet from the list to load it into this page.

39 |

Download the sample html file and css file

40 |
41 | 42 |
43 |

The Road to Enlightenment

44 |

Littering a dark and dreary road lay the past relics of browser-specific tags, incompatible DOMs, and broken CSS support.

45 |

Today, we must clear the mind of past practices. Web enlightenment has been achieved thanks to the tireless efforts of folk like the W3C, WaSP and the major browser creators.

46 |

The css Zen Garden invites you to relax and meditate on the important lessons of the masters. Begin to see with clarity. Learn to use the (yet to be) time-honored techniques in new and invigorating fashion. Become one with the web.

47 |
48 |
49 | 50 |
51 |
52 |

So What is This About?

53 |

There is clearly a need for CSS to be taken seriously by graphic artists. The Zen Garden aims to excite, inspire, and encourage participation. To begin, view some of the existing designs in the list. Clicking on any one will load the style sheet into this very page. The code remains the same, the only thing that has changed is the external .css file. Yes, really.

54 |

CSS allows complete and total control over the style of a hypertext document. The only way this can be illustrated in a way that gets people excited is by demonstrating what it can truly be, once the reins are placed in the hands of those able to create beauty from structure. To date, most examples of neat tricks and hacks have been demonstrated by structurists and coders. Designers have yet to make their mark. This needs to change.

55 |
56 | 57 |
58 |

Participation

59 |

Graphic artists only please. You are modifying this page, so strong CSS skills are necessary, but the example files are commented well enough that even CSS novices can use them as starting points. Please see the CSS Resource Guide for advanced tutorials and tips on working with CSS.

60 |

You may modify the style sheet in any way you wish, but not the HTML. This may seem daunting at first if you’ve never worked this way before, but follow the listed links to learn more, and use the sample files as a guide.

61 |

Download the sample html file and css file to work on a copy locally. Once you have completed your masterpiece (and please, don’t submit half-finished work) upload your .css file to a web server under your control. Send us a link to the file and if we choose to use it, we will spider the associated images. Final submissions will be placed on our server.

62 |
63 | 64 |
65 |

Benefits

66 |

Why participate? For recognition, inspiration, and a resource we can all refer to when making the case for CSS-based design. This is sorely needed, even today. More and more major sites are taking the leap, but not enough have. One day this gallery will be a historical curiosity; that day is not today.

67 |
68 | 69 |
70 |

Requirements

71 |

We would like to see as much CSS1 as possible. CSS2 should be limited to widely-supported elements only. The css Zen Garden is about functional, practical CSS and not the latest bleeding-edge tricks viewable by 2% of the browsing public. The only real requirement we have is that your CSS validates.

72 |

Unfortunately, designing this way highlights the flaws in the various implementations of CSS. Different browsers display differently, even completely valid CSS at times, and this becomes maddening when a fix for one leads to breakage in another. View the Resources page for information on some of the fixes available. Full browser compliance is still sometimes a pipe dream, and we do not expect you to come up with pixel-perfect code across every platform. But do test in as many as you can. If your design doesn’t work in at least IE5+/Win and Mozilla (run by over 90% of the population), chances are we won’t accept it.

73 |

We ask that you submit original artwork. Please respect copyright laws. Please keep objectionable material to a minimum; tasteful nudity is acceptable, outright pornography will be rejected.

74 |

This is a learning exercise as well as a demonstration. You retain full copyright on your graphics (with limited exceptions, see submission guidelines), but we ask you release your CSS under a Creative Commons license identical to the one on this site so that others may learn from your work.

75 |

Bandwidth graciously donated by mediatemple. Now available: Zen Garden, the book. 

76 |
77 | 78 | 85 | 86 |
87 | 88 | 89 | 124 | 125 | 126 |
127 | 128 | 129 |
130 |
131 | 132 | 133 | -------------------------------------------------------------------------------- /examples/simple.php: -------------------------------------------------------------------------------- 1 | class('p1'); 31 | $template['#preamble .p2'] = 'The key concept to understand Respect\Template is simple: isolation of concerns. Backend developers should handle data and serve them properly while frontend developers should do the shinning stuff (CSS and Javascript).'; 32 | $template['#preamble .p3'] = 'Respect\Template aims to be easy to use by everyone, with minimum learning curve, minimal impact on the existing code and awesome as possible!'; 33 | 34 | echo $template->render(); 35 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapter.php: -------------------------------------------------------------------------------- 1 | adapters[$class] = new $class(); 19 | } 20 | } 21 | 22 | public static function getInstance() 23 | { 24 | if (self::$instance instanceof Adapter) { 25 | return self::$instance; 26 | } 27 | 28 | return self::$instance = new Adapter(); 29 | } 30 | 31 | public static function factory(DOMDocument $dom, $content) 32 | { 33 | return self::getInstance()->_factory($dom, $content); 34 | } 35 | 36 | public function _factory(DOMDocument $dom, $content) 37 | { 38 | if ($content instanceof AbstractAdapter) { 39 | return $content; 40 | } 41 | 42 | foreach ($this->adapters as $class => $object) { 43 | if ($object->isValidData($content)) { 44 | return new $class($dom, $content); 45 | } 46 | } 47 | 48 | $type = gettype($content); 49 | $type .= (!is_object($type)) ? '' : ' of class '.get_class($content); 50 | throw new UnexpectedValueException('No adapter found for '.$type); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapters/A.php: -------------------------------------------------------------------------------- 1 | hasProperty($data, 'href')) { 13 | return true; 14 | } 15 | 16 | return false; 17 | } 18 | 19 | protected function getDomNode($data, DOMNode $parent) 20 | { 21 | $element = $this->createElement($parent, 'a'); 22 | $element->setAttribute('href', $this->getProperty($data, 'href')); 23 | if ($this->hasProperty($data, 'innerHtml')) { 24 | $inner = $this->getProperty($data, 'innerHtml'); 25 | $adapter = Adapter::factory($this->getDom(), $inner); 26 | new Append($element, $adapter); 27 | } 28 | 29 | return $element; 30 | } 31 | 32 | public function getDecorator() 33 | { 34 | return 'Respect\Template\Decorators\Replace'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapters/AbstractAdapter.php: -------------------------------------------------------------------------------- 1 | dom = $dom; 20 | } 21 | 22 | if (!is_null($content)) { 23 | $this->content = $content; 24 | } 25 | } 26 | 27 | abstract public function isValidData($data); 28 | abstract protected function getDomNode($data, DOMNode $parent); 29 | 30 | public function adaptTo(DOMNode $parent, $content = null) 31 | { 32 | $content = ($content) ? $content : $this->content; 33 | 34 | return $this->getDomNode($content, $parent); 35 | } 36 | 37 | protected function createElement(DOMNode $parent, $name, $value = null) 38 | { 39 | if (!$this->dom instanceof DOMDocument) { 40 | throw new UnexpectedValueException('No DOMDocument, cannot create new element'); 41 | } 42 | 43 | return $this->dom->createElement($name, $value); 44 | } 45 | 46 | final protected function hasProperty($data, $name) 47 | { 48 | if (is_array($data)) { 49 | return isset($data[$name]); 50 | } 51 | 52 | if (is_object($data)) { 53 | return isset($data->$name); 54 | } 55 | 56 | return false; 57 | } 58 | 59 | public function getDecorator() 60 | { 61 | return 'Respect\Template\Decorators\CleanAppend'; 62 | } 63 | 64 | /** 65 | * @return DOMDocument 66 | */ 67 | public function getDom() 68 | { 69 | return $this->dom; 70 | } 71 | 72 | final protected function getProperty($data, $name) 73 | { 74 | if (is_array($data)) { 75 | return $data[$name]; 76 | } 77 | 78 | if (is_object($data)) { 79 | return $data->$name; 80 | } 81 | 82 | return; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapters/Dom.php: -------------------------------------------------------------------------------- 1 | getDOMNode($this->getDom()); 16 | } 17 | 18 | public function getDecorator() 19 | { 20 | return 'Respect\Template\Decorators\Replace'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapters/String.php: -------------------------------------------------------------------------------- 1 | hasMethod('__toString'); 21 | } 22 | 23 | protected function getDomNode($data, DOMNode $parent) 24 | { 25 | return new DOMText($data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /library/Respect/Template/Adapters/Traversable.php: -------------------------------------------------------------------------------- 1 | getChildTag($parent); 16 | $container = $parent->ownerDocument->createDocumentFragment(); 17 | foreach ($data as $analyse) { 18 | $value = (is_array($analyse)) ? null : $analyse; 19 | $child = $this->createElement($parent, $tag, $value); 20 | $container->appendChild($child); 21 | if (is_array($analyse)) { 22 | $child->appendChild($this->getDomNode($analyse, $child)); 23 | } 24 | } 25 | 26 | return $container; 27 | } 28 | 29 | protected function getChildTag(DOMNode $node) 30 | { 31 | switch ($node->nodeName) { 32 | case 'ol': 33 | case 'ul': 34 | case 'li': 35 | return 'li'; 36 | break; 37 | case 'tbody': 38 | case 'table': 39 | case 'thead': 40 | return 'tr'; 41 | break; 42 | case 'tr': 43 | return 'td'; 44 | break; 45 | default: 46 | return 'span'; 47 | break; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /library/Respect/Template/Decorators/AbstractDecorator.php: -------------------------------------------------------------------------------- 1 | getResult(); 20 | } 21 | 22 | if (!is_array($elements)) { 23 | throw new InvalidArgumentException('Query or Array expected to decorate'); 24 | } 25 | 26 | // Decorate the given elements selected 27 | foreach ($elements as $element) { 28 | if (!$element instanceof DOMNode) { 29 | throw new UnexpectedValueException('DOMNode expected for decoration'); 30 | } 31 | 32 | if (!is_null($with)) { 33 | $with = $with->adaptTo($element); 34 | } 35 | $this->decorate($element, $with); 36 | } 37 | } 38 | 39 | abstract protected function decorate(DOMNode $node, DOMNode $with = null); 40 | } 41 | -------------------------------------------------------------------------------- /library/Respect/Template/Decorators/Append.php: -------------------------------------------------------------------------------- 1 | appendChild($with); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /library/Respect/Template/Decorators/Clean.php: -------------------------------------------------------------------------------- 1 | childNodes as $child) { 12 | $remove[] = $child; 13 | } 14 | 15 | foreach ($remove as $child) { 16 | $node->removeChild($child); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /library/Respect/Template/Decorators/CleanAppend.php: -------------------------------------------------------------------------------- 1 | appendChild($with); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /library/Respect/Template/Decorators/Replace.php: -------------------------------------------------------------------------------- 1 | ownerDocument->importNode($with, true); 13 | $return = $old->parentNode->replaceChild($new, $old); 14 | if ($return !== $old) { 15 | throw new UnexpectedValueException('Unable to replace node'); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/Respect/Template/Document.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Document 16 | { 17 | /**#@+ 18 | * Constants to define the docnype of the document. 19 | * 20 | * @link http://www.w3.org/QA/2002/04/valid-dtd-list.html 21 | * @author Nick Lombard 22 | */ 23 | const HTML_5 = <<<'HTML' 24 | 25 | HTML; 26 | const HTML_4_01_STRICT = <<<'EOD' 27 | 29 | EOD; 30 | const HTML_4_01_TRANSITIONAL = <<<'EOD' 31 | 33 | EOD; 34 | const HTML_4_01_FRAMESET = <<<'EOD' 35 | 37 | EOD; 38 | const XHTML_1_0_STRICT = <<<'EOD' 39 | 41 | EOD; 42 | const XHTML_1_0_TRANSITIONAL = <<<'EOD' 43 | 45 | EOD; 46 | const XHTML_1_0_FRAMESET = <<<'EOD' 47 | 49 | EOD; 50 | const XHTML_1_1 = <<<'EOD' 51 | 53 | EOD; 54 | const XHTML_1_1_BASIC = <<<'EOD' 55 | 57 | EOD; 58 | const MATHML_2_0 = <<<'EOD' 59 | 61 | EOD; 62 | const MATHML_1_01 = <<<'EOD' 63 | 65 | EOD; 66 | const XHTML_MATHML_SVG = <<<'EOD' 67 | 70 | EOD; 71 | const XHTML_MATHML_SVG_PROFILE_XHTML = <<<'EOD' 72 | 75 | EOD; 76 | const XHTML_MATHML_SVG_PROFILE_SVG = <<<'EOD' 77 | 80 | EOD; 81 | const SVG_1_1 = <<<'EOD' 82 | 84 | EOD; 85 | const SVG_1_0 = <<<'EOD' 86 | 88 | EOD; 89 | const SVG_1_1_BASIC = <<<'EOD' 90 | 92 | EOD; 93 | const SVG_1_1_TINY = <<<'EOD' 94 | 96 | EOD; 97 | const HTML_2_0 = <<<'EOD' 98 | 99 | EOD; 100 | const HTML_3_2 = <<<'EOD' 101 | 102 | EOD; 103 | const XHTML_1_0_BASIC = <<<'EOD' 104 | 106 | EOD; 107 | /**#@-*/ 108 | 109 | /** 110 | * @var DOMDocument 111 | */ 112 | private $dom; 113 | /** 114 | * @var Zend_Dom_Query 115 | */ 116 | private $queryDocument; 117 | 118 | /** 119 | * @param string $htmlDocument 120 | */ 121 | public function __construct($htmlDocument) 122 | { 123 | $this->dom = new DOMDocument(); 124 | $this->dom->strictErrorChecking = false; 125 | $this->dom->loadHtml($htmlDocument); 126 | } 127 | 128 | /** 129 | * @return DOMDocument 130 | */ 131 | public function getDom() 132 | { 133 | return $this->dom; 134 | } 135 | 136 | /** 137 | * Replaces this dom content with the given array. 138 | * The array structure is: $array['Css Selector to Eelement'] = 'content'; 139 | * 140 | * @param array $data 141 | * @param string[optional] $decorator Class to be used as decorator 142 | * @return Respect\Template\Document 143 | */ 144 | public function decorate(array $data, $decorator = null) 145 | { 146 | foreach ($data as $selector => $with) { 147 | $adapter = Adapter::factory($this->getDom(), $with); 148 | $decorator = $decorator ?: $adapter->getDecorator(); 149 | $query = new Query($this, $selector); 150 | new $decorator($query, $adapter); 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Returns the XML representation of the current DOM tree. 158 | * 159 | * @param boolean $beautiful - to pretty print or not 160 | * @param string $doctype - the doctype to use 161 | * @return string 162 | */ 163 | public function render($beautiful = false, $doctype = '') 164 | { 165 | $this->dom->formatOutput = $beautiful; 166 | 167 | if ($doctype) { 168 | $doc = new DOMDocument(); 169 | $doc->loadHTML($doctype); 170 | $dt = $doc->doctype; 171 | $di = new DOMImplementation(); 172 | $dt = $di->createDocumentType($dt->name, $dt->publicId, $dt->systemId); 173 | $this->dom->replaceChild($dt, $this->dom->doctype); 174 | } 175 | 176 | return preg_replace('/<\?xml[\s\S]*\?>\n/', '', $this->dom->saveXML()); 177 | } 178 | 179 | /** 180 | * Returns XML to be parsed by CSS the selector. 181 | * This will never be the final XML to be rendered. 182 | * 183 | * @return string 184 | */ 185 | public function getQueryDocument() 186 | { 187 | if (! $this->queryDocument) { 188 | return $this->queryDocument = new DomQuery($this->render()); 189 | } 190 | 191 | return $this->queryDocument; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /library/Respect/Template/Html.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | const HTML_5 = Document::HTML_5; 15 | const HTML_4_01_STRICT = Document::HTML_4_01_STRICT; 16 | const HTML_4_01_TRANSITIONAL = Document::HTML_4_01_TRANSITIONAL; 17 | const HTML_4_01_FRAMESET = Document::HTML_4_01_FRAMESET; 18 | const XHTML_1_0_STRICT = Document::XHTML_1_0_STRICT; 19 | const XHTML_1_0_TRANSITIONAL = Document::XHTML_1_0_TRANSITIONAL; 20 | const XHTML_1_0_FRAMESET = Document::XHTML_1_0_FRAMESET; 21 | const XHTML_1_1 = Document::XHTML_1_1; 22 | const XHTML_1_1_BASIC = Document::XHTML_1_1_BASIC; 23 | const HTML_2_0 = Document::HTML_2_0; 24 | const HTML_3_2 = Document::HTML_3_2; 25 | const XHTML_1_0_BASIC = Document::XHTML_1_0_BASIC; 26 | /**#@-*/ 27 | 28 | /** 29 | * @var Respect\Template\Document 30 | */ 31 | protected $document; 32 | public $aliasFor = array(); 33 | 34 | public function __construct($templateFileOrString) 35 | { 36 | if (file_exists($templateFileOrString)) { 37 | $content = file_get_contents($templateFileOrString); 38 | } else { 39 | $content = $templateFileOrString; 40 | } 41 | $this->document = new Document($content); 42 | } 43 | 44 | public function __toString() 45 | { 46 | return $this->render(); 47 | } 48 | 49 | public function inheritFrom(Html $model, $blockSelector, $anotherSelector = null, $etc = null) 50 | { 51 | $selectors = array_slice(func_get_args(), 1); 52 | foreach ($selectors as $selector) { 53 | foreach ($model->find($selector) as $modelNode) { 54 | foreach ($this->find($selector) as $targetNode) { 55 | $targetNode->parentNode->replaceChild( 56 | $this->document->getDom()->importNode($modelNode, true), 57 | $targetNode 58 | ); 59 | } 60 | } 61 | } 62 | } 63 | 64 | public function getDocument() 65 | { 66 | return $this->document; 67 | } 68 | 69 | /** 70 | * @return array 71 | */ 72 | public function find($selector) 73 | { 74 | $query = new Query($this->document, $selector); 75 | 76 | return $query->getResult(); 77 | } 78 | 79 | /** 80 | * Returns the HTML document. 81 | * 82 | * @param array $data - The data to render 83 | * @param boolean $beatiful - To pretty print or not 84 | * @param string $doctype - The doctype of this html 85 | * @return string 86 | */ 87 | public function render($data = null, $beatiful = false, $doctype = '') 88 | { 89 | foreach ($this->aliasFor as $selector => $alias) { 90 | $this[$selector] = $this[$alias]; 91 | } 92 | 93 | $data = $data ?: $this->getArrayCopy(); 94 | 95 | return $this->document->decorate($data)->render($beatiful, $doctype); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /library/Respect/Template/HtmlElement.php: -------------------------------------------------------------------------------- 1 | nodeName = $name; 16 | $this->childrenNodes = $children; 17 | } 18 | 19 | public function __call($attribute, $value) 20 | { 21 | $attribute = strtolower(preg_replace('/[A-Z]/', '-$0', $attribute)); 22 | $this->attributes[$attribute] = $value[0]; 23 | 24 | return $this; 25 | } 26 | 27 | public static function __callStatic($name, array $children) 28 | { 29 | return new static($name, $children); 30 | } 31 | 32 | public function __toString() 33 | { 34 | $children = implode($this->childrenNodes); 35 | $attrs = ''; 36 | foreach ($this->attributes as $attr => &$value) { 37 | $attrs .= " $attr=\"$value\""; 38 | } 39 | 40 | if (count($this->childrenNodes)) { 41 | return "<{$this->nodeName}{$attrs}>$childrennodeName}>"; 42 | } 43 | 44 | return "<{$this->nodeName}{$attrs} />"; 45 | } 46 | 47 | public function getDOMNode(DOMDocument $dom = null, $current = null) 48 | { 49 | if (is_null($dom)) { 50 | $dom = new DOMDocument(); 51 | } 52 | 53 | if (is_string($current)) { 54 | return new DOMText($current); 55 | } 56 | 57 | $current = $current ?: $this; 58 | $node = $dom->createElement($current->nodeName); 59 | foreach ($current->attributes as $name => $value) { 60 | $node->setAttribute($name, $value); 61 | } 62 | 63 | if (!count($current->childrenNodes)) { 64 | return $node; 65 | } 66 | 67 | foreach ($current->childrenNodes as $child) { 68 | $node->appendChild($this->getDOMNode($dom, $child)); 69 | } 70 | 71 | return $node; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /library/Respect/Template/Query.php: -------------------------------------------------------------------------------- 1 | document = $doc; 26 | $this->selector = $selector; 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getResult() 33 | { 34 | // Get results by a CSS selector 35 | $selector = $this->selector; 36 | $document = $this->document->getQueryDocument(); 37 | $results = $document->execute($selector); 38 | $xpath = $results->getXpathQuery(); 39 | $domxpath = new DOMXPath($this->document->getDom()); 40 | $nodelist = iterator_to_array($domxpath->query($xpath)); 41 | 42 | return $nodelist; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | library/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/library/Respect/Template/AdapterTest.php: -------------------------------------------------------------------------------- 1 | dom = new DOMDocument('1.0', 'iso-8859-1'); 15 | } 16 | public function testFactoryException() 17 | { 18 | $this->setExpectedException('UnexpectedValueException'); 19 | Adapter::factory($this->dom, new Pdo('sqlite::memory:')); 20 | } 21 | 22 | 23 | public function instances() 24 | { 25 | $anchorObject = new StdClass(); 26 | $anchorObject->href = 'test'; 27 | return array( 28 | array('A', array('href'=>'test'), 'Replace'), 29 | array('A', $anchorObject, 'Replace'), 30 | array('Dom', new DOMText, 'CleanAppend'), 31 | array('String', 'Hello World!', 'CleanAppend'), 32 | array('Traversable', array('one', 'two', 'three', 'pigs'), 'CleanAppend'), 33 | array('HtmlElement', H::ul(H::li('one')), 'Replace') 34 | ); 35 | } 36 | /** 37 | * @dataProvider instances 38 | */ 39 | public function testInstances($className, $content, $decorator) 40 | { 41 | $adapter = Adapter::factory($this->dom, $content); 42 | $className = 'Respect\Template\Adapters\\'.$className; 43 | $decorator = 'Respect\Template\Decorators\\'.$decorator; 44 | $this->assertInstanceOf($className, $adapter); 45 | $this->assertEquals($decorator, $adapter->getDecorator()); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/library/Respect/Template/Adapters/ATest.php: -------------------------------------------------------------------------------- 1 | dom = new DOMDocument('1.0', 'iso-8859-1'); 16 | } 17 | 18 | public function testReplaceDecorator() 19 | { 20 | $object = new Adapter(); 21 | $class = $object->getDecorator(); 22 | $this->assertEquals('Respect\Template\Decorators\Replace', $class); 23 | } 24 | 25 | public function testSimpleAdaptation() 26 | { 27 | $href = '#top'; 28 | $from = array('href'=>$href); 29 | $adapter = A::factory($this->dom, $from); 30 | $this->assertInstanceOf('Respect\Template\Adapters\A', $adapter); 31 | $to = $adapter->adaptTo($this->dom); 32 | $this->assertInstanceOf('DOMElement', $to); 33 | $this->assertTrue($to->hasAttribute('href')); 34 | $this->assertEquals($href, $to->getAttribute('href')); 35 | } 36 | 37 | public function testAdaptationWithInnerHtml() 38 | { 39 | $std = new StdClass(); 40 | $std->href = '#top'; 41 | $std->innerHtml = 'Top'; 42 | $adapter = A::factory($this->dom, $std); 43 | $this->assertInstanceOf('Respect\Template\Adapters\A', $adapter); 44 | $to = $adapter->adaptTo($this->dom); 45 | $this->assertInstanceOf('DOMElement', $to); 46 | $this->assertTrue($to->hasAttribute('href')); 47 | 48 | $expected = $this->dom->createElement('a', 'Top'); 49 | $expected->setAttribute('href', '#top'); 50 | $this->assertEquals($expected, $to); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/library/Respect/Template/Adapters/HtmlElementTest.php: -------------------------------------------------------------------------------- 1 | dom = new DOMDocument('1.0', 'iso-8859-1'); 14 | } 15 | 16 | public function testInstance() 17 | { 18 | $expected = 'Respect\Template\Adapters\AbstractAdapter'; 19 | $this->assertInstanceOf($expected, new Adapter()); 20 | } 21 | 22 | public function testFactory() 23 | { 24 | $test = Factory::factory($this->dom, H::ul()); 25 | $expected = 'Respect\Template\Adapters\HtmlElement'; 26 | $this->assertInstanceOf($expected, $test); 27 | } 28 | 29 | public function testDecorator() 30 | { 31 | $expected = 'Respect\Template\Decorators\Replace'; 32 | $adapter = new Adapter(); 33 | $this->assertEquals($expected, $adapter->getDecorator()); 34 | } 35 | 36 | public function testAdaptation() 37 | { 38 | $parent = $this->dom->createElement('div'); 39 | $expected = $this->dom->createElement('ul'); 40 | $base = H::ul(); 41 | $adapter = Factory::factory($this->dom, $base); 42 | $this->assertInstanceOf('Respect\Template\Adapters\HtmlElement', $adapter); 43 | $this->assertEquals($expected, $adapter->adaptTo($parent)); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/library/Respect/Template/Adapters/StringTest.php: -------------------------------------------------------------------------------- 1 | dom = new DOMDocument('1.0', 'iso-8859-1'); 11 | } 12 | 13 | public function testInstance() 14 | { 15 | $this->assertInstanceOf('Respect\Template\Adapters\AbstractAdapter', new String()); 16 | } 17 | 18 | public function testSimpleAdaptation() 19 | { 20 | $div = $this->dom->createElement('div'); 21 | $div->setAttribute('id', 'test'); 22 | $this->dom->appendChild($div); 23 | 24 | $string = 'This is a simple string'; 25 | $adapter = new String($this->dom); 26 | $this->assertTrue($adapter->isValidData($string)); 27 | $adapted = $adapter->adaptTo($div, $string); 28 | $div->appendChild($adapted); 29 | $this->assertInstanceOf('DOMText', $adapted); 30 | $this->assertContains('
This is a simple string
', $this->dom->saveXml()); 31 | } 32 | } -------------------------------------------------------------------------------- /tests/library/Respect/Template/Adapters/TraversableTest.php: -------------------------------------------------------------------------------- 1 | dom = new DOMDocument('1.0', 'iso-8859-1'); 10 | } 11 | 12 | protected function assertContainTraversables($traversables) 13 | { 14 | $xml = $this->dom->saveXml(); 15 | foreach ($traversables as $item) { 16 | if (is_string($item)) 17 | $this->assertContains($item, $xml, "'{$item}' not found in: ".$xml); 18 | if (is_array($item)) 19 | $this->assertContainTraversables($item); 20 | } 21 | } 22 | 23 | public function testInstance() 24 | { 25 | $this->assertInstanceOf('Respect\Template\Adapters\AbstractAdapter', new Traversable()); 26 | } 27 | 28 | public function testSimpleAdaptation() 29 | { 30 | $ul = $this->dom->createElement('ul'); 31 | $ul->setAttribute('id', 'test'); 32 | $this->dom->appendChild($ul); 33 | 34 | $array = array('one', 'two', 'three'); 35 | $adapter = new Traversable($this->dom); 36 | $this->assertTrue($adapter->isValidData($array)); 37 | $adapted = $adapter->adaptTo($ul, $array); 38 | $ul->appendChild($adapted); 39 | $this->assertInstanceOf('DOMDocumentFragment', $adapted); 40 | 41 | $xml = $this->dom->saveXml(); 42 | $this->assertContains('
  • one
  • ', $xml); 43 | $this->assertContains('
  • two
  • ', $xml); 44 | $this->assertContains('
  • three
  • ', $xml); 45 | $this->assertContains('', array('#test'=>'Hi'), ''), 10 | array('', array('#test'=>array('A','B')), '') 11 | ); 12 | } 13 | 14 | /** 15 | * @dataProvider constructs 16 | */ 17 | public function testSetTemplate($template, $data, $expected) 18 | { 19 | $view = new Html($template); 20 | $out = $view->render(); 21 | $this->assertContains($template, $out, "Output does not have the defined template: {$out}"); 22 | } 23 | 24 | /** 25 | * @depends testSetTemplate 26 | * @dataProvider constructs 27 | */ 28 | public function testPassingDataToRender($template, $data, $expected) 29 | { 30 | $view = new Html($template); 31 | $result = $view->render($data); 32 | $this->assertContains($expected, $result); 33 | } 34 | 35 | /** 36 | * @depends testPassingDataToRender 37 | * @dataProvider constructs 38 | */ 39 | public function testTemplateWithArrayObject($template, $data, $expected) 40 | { 41 | $view = new Html($template); 42 | foreach ($data as $key=>$val) 43 | $view[$key] = $val; 44 | 45 | $this->assertEquals($data, $view->getArrayCopy(), 'ArrayObject did not work to set data for template'); 46 | $this->assertContains($expected, (string) $view); 47 | } 48 | 49 | public function testArrayAccessInterface() 50 | { 51 | $template = new Html('
    '); 52 | $this->assertInstanceOf('ArrayAccess', $template); 53 | $this->assertFalse(isset($template['one'])); 54 | $template['one'] = 'one'; 55 | $this->assertTrue(isset($template['one'])); 56 | $this->assertEquals('one', $template['one']); 57 | unset($template['one']); 58 | $this->assertFalse(isset($template['one'])); 59 | } 60 | 61 | public function testResolveAliases() 62 | { 63 | $v = new Html("\n

    \n"); 64 | $v->aliasFor["h1 > span"] = "pagetitle"; 65 | $v["pagetitle"] = "FooBar"; 66 | $this->assertEquals("\n

    FooBar

    \n", (string) $v); 67 | } 68 | 69 | public function testSimpleInheritance() 70 | { 71 | $template = new Html("\n

    My Text
    \n"); 72 | $model = new Html("

    Hi!

    \n"); 73 | $template->inheritFrom($model, "h1"); 74 | $this->assertEquals("\n

    Hi!

    My Text
    \n", (string) $template); 75 | } 76 | } 77 | --------------------------------------------------------------------------------