keep_prolog = true; //load the template file to work with. this must be valid XML (but not XHTML) $this->DOMDocument = new DOMDocument (); $this->DOMDocument->loadXML ( //if the document doesn't already have an XML prolog, add one to avoid mangling unicode characters //see (!$this->keep_prolog ? "" : ''). //replace HTML entities (e.g. "©") with real unicode characters to prevent invalid XML self::html_entity_decode ($xml), @LIBXML_COMPACT | @LIBXML_NONET ) or trigger_error ( "Template '$filepath' is invalid XML", E_USER_ERROR ); //set the root node for all xpath searching //(handled all internally by `DOMTemplateNode`) parent::__construct ($this->DOMDocument->documentElement, $NS, $NS_URI); } //output the complete HTML public function html () { //fix and clean DOM's XML output: return preg_replace ( //add space to self-closing //fix broken self-closed tags array ('/<(.*?[^ ])\/>/s', '/<(div|[ou]l|textarea)(.*?) ?\/>/'), array ('<$1 />', '<$1$2>'), //should we remove the XML prolog? !$this->keep_prolog ? preg_replace ('/^<\?xml.*?>\n/', '', $this->DOMDocument->saveXML ()) : $this->DOMDocument->saveXML () ); } } //these functions are shared between the base `DOMTemplate` and the repeater `DOMTemplateRepeater`, //the DOM/XPATH voodoo is encapsulated here class DOMTemplateNode { protected $DOMNode; private $DOMXPath; protected $NS; //namespace protected $NS_URI; //namespace URI //because everything is XML, HTML named entities like "©" will cause blank output. //we need to convert these named entities back to real UTF-8 characters (which XML doesn’t mind) //'&', '<' and '>' are exlcuded so that we don’t turn user text into working HTML! public static $entities = array ( //BTW, if you have PHP 5.3.4+ you can produce this whole array with just two lines of code: // // $entities = array_flip (get_html_translation_table (HTML_ENTITIES, ENT_NOQUOTES, 'UTF-8')); // unset ($entities['&'], $entities['<'], $entities['>']); // //also, this list is *far* from comprehensive. see this page for the full list //http://www.whatwg.org/specs/web-apps/current-work/multipage/named-character-references.html ' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§', '¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯', '°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·', '¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿', 'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Æ' => 'Æ', 'Ç' => 'Ç', 'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï', 'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×', 'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß', 'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç', 'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï', 'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷', 'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ', 'Œ' => 'Œ', 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ƒ' => 'ƒ', 'ˆ' => 'ˆ', '˜' => '˜', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '•' => '•', '…' => '…', '‰' => '‰', '′' => '′', '″' => '″', '‹' => '‹', '›' => '›', '‾' => '‾', '⁄' => '⁄', '€' => '€', 'ℑ' => 'ℑ', '℘' => '℘', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦' ); public static function html_entity_decode ($html) { return str_replace (array_keys (self::$entities), array_values (self::$entities), $html); } public function __construct ($DOMNode, $NS = '', $NS_URI = '') { //use a DOMNode as a base point for all the XPath queries and whatnot //(in DOMTemplate this will be the whole template, in DOMTemplateRepeater, it will be the chosen element) $this->DOMNode = $DOMNode; $this->DOMXPath = new DOMXPath ($DOMNode->ownerDocument); //the painful bit. if you have an XMLNS in your template then XPath won’t work unless you: // a. register a default namespace, and // b. prefix all your XPath queries with this namespace $this->NS = $NS; $this->NS_URI = $NS_URI; if ($this->NS && $this->NS_URI) $this->DOMXPath->registerNamespace ($this->NS, $this->NS_URI); } //actions are performed on elements using xpath, but for brevity a shorthand is also recognised in the format of: // #id - find an element with a particular ID (instead of writing './/*[@id="…"]') // .class - find an element with a particular class // element#id - enforce a particular element type (ID or class supported) // #id@attr - select the named attribute of the found element // element#id@attr - a fuller example public function query ($query) { //multiple targets are available by comma separating queries $queries = explode (', ', $query); //convert each query to real XPath: foreach ($queries as &$query) if ( //is this the shorthand syntax? preg_match ('/^([a-z0-9-]+)?([\.#])([a-z0-9:_-]+)(@[a-z-]+)?$/i', $query, $m) ) $query = './/'. //see ($this->NS ? $this->NS.':' : ''). //the default namespace, if set (@$m[1] ? $m[1] : '*'). //the element name, if specified, otherwise "*" ($m[2] == '#' //is this an ID? ? "[@id=\"${m[3]}\"]" //- yes : "[contains(@class,\"${m[3]}\")]" //- no, a class ). (@$m[4] ? "/${m[4]}" : '') //optional attribute of the parent element ; //run the real XPath query and return the nodelist result return $this->DOMXPath->query (implode ('|', $queries), $this->DOMNode); } //specify an element to repeat (like a list-item): //this will return an DOMTemplateRepeater class that allows you to modify the contents the same as with the base //template but also append the results to the parent and return to the original element's content to go again public function repeat ($query) { //take just the first element found in a query and return a repeating template of the element return new DOMTemplateRepeater ($this->query ($query)->item (0), $this->NS, $this->NS_URI); } //this sets multiple values using multiple xpath queries public function set ($queries) { foreach ($queries as $query => $value) $this->setValue ($query, $value); return $this; } //set the text content on the results of a single xpath query public function setValue ($query, $value) { foreach ($this->query ($query) as $node) $node->nodeValue = $node->nodeType == XML_ATTRIBUTE_NODE ? htmlspecialchars ($value, ENT_QUOTES) : htmlspecialchars ($value, ENT_NOQUOTES) ; return $this; } //set HTML content for a single xpath query public function setHTML ($query, $html) { foreach ($this->query ($query) as $node) { $frag = $node->ownerDocument->createDocumentFragment (); //if the HTML string is not valid, it won’t work $frag->appendXML (self::html_entity_decode ($html)); $node->nodeValue = ''; $node->appendChild ($frag); } return $this; } public function addClass ($query, $new_class) { //first determine if there is a 'class' attribute already? foreach ($this->query ($query) as $node) if ( $node->hasAttributes () && $class = $node->getAttribute ('class') ) { //if the new class is not already in the list, add it in if (!in_array ($new_class, explode (' ', $class))) $node->setAttribute ('class', "$class $new_class") ; } else { //no class attribute to begin with, add it $node->setAttribute ('class', $new_class); } return $this; } //remove all the elements / attributes that match an xpath query public function remove ($query) { //this function can accept either a single query, or an array in the format of `'xpath' => true|false`. //if the value is true then the xpath will be run and the found elements deleted, if the value is false //then the xpath is skipped. why on earth would you want to provide an xpath, but not run it? because //you can compact your code by using logic comparisons for the value if (is_string ($query)) $query = array ($query => true); foreach ($query as $xpath => $logic) if ($logic) foreach ($this->query ($xpath) as $node) if ( $node->nodeType == XML_ATTRIBUTE_NODE ) { $node->parentNode->removeAttributeNode ($node); } else { $node->parentNode->removeChild ($node); } return $this; } } //using `DOMTemplate->repeat ('xpath');` returns one of these classes that acts as a sub-template that you can modify and //then call the `next` method to append it to the parent and return to the template's original HTML code. this makes //creating a list stunning simple! e.g. /* $item = $DOMTemplate->repeat ('.list-item'); foreach ($data as $value) { $item->setValue ('.item-name', $value); $item->next (); } */ class DOMTemplateRepeater extends DOMTemplateNode { private $refNode; private $template; public function __construct ($DOMNode, $NS = '', $NS_URI = '') { //we insert the templated item before or after the reference node, //which will always be set to the last item that was templated $this->refNode = $DOMNode; //take a copy of the original node that we will use as a starting point each time we iterate $this->template = $DOMNode->cloneNode (true); //initialise the template with the current, original node parent::__construct ($DOMNode, $NS, $NS_URI); } public function next () { //when we insert the newly templated item, use it as the reference node for the next item and so on. $this->refNode = ($this->refNode->parentNode->lastChild === $this->DOMNode) ? $this->refNode->parentNode->appendChild ($this->DOMNode) //if there's some kind of HTML after the reference node, we can use that to insert our item //inbetween. this means that the list you are templating doesn't have to be wrapped in an element! : $this->refNode->parentNode->insertBefore ($this->DOMNode, $this->refNode->nextSibling) ; //reset the template $this->DOMNode = $this->template->cloneNode (true); } } ?>