<?php //DOM Templating classes v4 © copyright (cc-by) Kroc Camen 2012 //you may do whatever you want with this code as long as you give credit //documentation at http://camendesign.com/dom_templating class DOMTemplate extends DOMTemplateNode { private $DOMDocument; public function __construct ($filepath) { //load the template file to work with $this->DOMDocument = new DOMDocument (); $this->DOMDocument->loadXML ( //replace HTML entities (e.g. "©") with real unicode characters to prevent invalid XML static::html_entity_decode (file_get_contents ($filepath)), 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); } //output the complete HTML public function html () { //remove remaining `data-template` attributes $this->remove ('xpath://@data-template'); //fix and clean DOM's XML output: return preg_replace (array ( '/^<\?xml.*?>\n/', //1: remove XML prolog '/<(.*?[^ ])\/>/s', //2: add space to self-closing '/<(div|[ou]l|textarea)(.*?) ?\/>/' //3: fix broken self-closed tags ), array ( '', '<$1 />', '<$1$2></$1>' ), $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; //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 (static::$entities), array_values (static::$entities), $html); } public function __construct ($DOMNode) { //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); } /* templating works by putting `data-template="xyz"` attributes on the HTML elements in your templates, and then using xpath to refer to these elements and change their values and contents, this means that the vast majority of xpath queries are in the format of `.//*[@data-template="xyz"]`. because of this, a shorthand format is provided by default: element:template-name@attribute for eaxmple "a:xyz@href" would be translated to `.//a[@data-template="xyz"]/@href`. the "element:" and "@attribute" parts are optional. since this type of syntax is default, prefix the query with "xpath:" to use a full, real XPath query */ 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 (substr ($query, 0, 6) == "xpath:") { //return the query without the "xpath:" prefix $query = substr ($query, 6); } else { //match the query against the shorthand syntax if (preg_match ('/^(?:([a-z0-9-]+):)?([a-z0-9_-]+)(@[a-z-]+)?$/i', $query, $m)) $query = ".//". //use relative: <php.net/manual/en/domxpath.query.php#99760> (@$m[1] ? $m[1] : "*"). //the element name, if specified, otherwise "*" '[@data-template="'.$m[2].'"]'. //the data-template attribute to find (@$m[3] ? '/'.$m[3] : '') //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 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 (static::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 $parent; private $template; public function __construct ($DOMNode) { //add a reference to the parent node, where we will be appending the children $this->parent = $DOMNode->parentNode; //take the original node to use as the template for reuse //and remove the source node from the original document $this->template = $DOMNode->cloneNode (true); $DOMNode->parentNode->removeChild ($DOMNode); //intitialise the repeater with a copy of the template parent::__construct ($this->template->cloneNode (true)); } public function next () { //attach the node to the parent $this->parent->appendChild ($this->DOMNode); //reset the template $this->DOMNode = $this->template->cloneNode (true); } } ?>