forked from Kroc/DOMTemplate
-
Notifications
You must be signed in to change notification settings - Fork 2
/
domtemplate.php
300 lines (276 loc) · 16.7 KB
/
domtemplate.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
<?php
//DOM Templating classes v9 © copyright (cc-by) Kroc Camen 2012
//you may do whatever you want with this code as long as you give credit
//documentation at <camendesign.com/dom_templating>
class DOMTemplate extends DOMTemplateNode {
private $DOMDocument;
private $keep_prolog = false;
public function __construct ($xml, $NS='', $NS_URI='') {
//does this file have an XML prolog? if so, we’ll keep it as-is in the output
if (substr_compare ($xml, '<?xml', 0, 4, true) === 0) $this->keep_prolog = true;
//load the template file to work with. this must be valid XML (but not XHTML)
$this->DOMDocument = new DOMDocument ();
if(!$this->DOMDocument->loadXML (
//if the document doesn't already have an XML prolog, add one to avoid mangling unicode characters
//see <php.net/manual/en/domdocument.loadxml.php#94291>
(!$this->keep_prolog ? "<?xml version=\"1.0\" encoding=\"utf-8\"?>" : '').
//replace HTML entities (e.g. "©") with real unicode characters to prevent invalid XML
self::html_entity_decode ($xml), @LIBXML_COMPACT | @LIBXML_NONET
)){
throw new Exception("Template is invalid XML");
}
// this trigger_error has been here before, but this kind of error does not seem to be catchable.
// A handler can be defined with register_shutdown_function() but this is not what we want. Or am I wrong?
/*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);
}
public static function fromFile($filename){
$inputString = file_get_contents ($filename);
return new DOMTemplate($inputString);
}
//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|script)(.*?) ?\/>/'),
array ('<$1 />', '<$1$2></$1>'),
//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);
}
//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
//note also:
//* you can test the value of attributes (e.g. '#id@attr="test"') this selects the element, not the attribute
//* sub-trees in shorthand can be expressed with '/', e.g. '#id/li/a@attr'
//* an index-number can be provided after the element name, e.g. 'li[1]'
public static function shorthand2xpath ($query, $apply_prefix=true) {
return preg_match (
'/^(?!\/)([a-z0-9:-]+(\[\d+\])?)?(?:([\.#])([a-z0-9:_-]+))?(@[a-z-]+(="[^"]+")?)?(?:\/(.*))?$/i',
$query, $m)
? ($apply_prefix ? './/' : ''). //see <php.net/manual/en/domxpath.query.php#99760>
(@$m[1] ? @$m[1].@$m[2] : '*'). //- the element name, if specified, otherwise "*"
(@$m[4] ? ($m[3] == '#' //is this an ID?
? "[@id=\"${m[4]}\"]" //- yes
: "[contains(@class,\"${m[4]}\")]" //- no, a class
) : '').
(@$m[5] ? (@$m[6] //optional attribute of the parent element
? "[${m[5]}]" //- an attribute test
: "/${m[5]}" //- or select the attribute
) : '').
(@$m[7] ? '/'.self::shorthand2xpath ($m[7], false) : '')
: $query;
}
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);
}
public function query ($query) {
//run the real XPath query and return the nodelist result
return $this->DOMXPath->query (implode ('|',
//convert each query to real XPath:
//(multiple targets are available by comma separating queries)
array_map (array ('self', 'shorthand2xpath'), explode (', ', $query))
), $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, $asHTML=false) {
foreach ($queries as $query => $value) $this->setValue ($query, $value, $asHTML); return $this;
}
//set the text content on the results of a single xpath query
public function setValue ($query, $value, $asHTML=false) {
foreach ($this->query ($query) as $node) switch (true) {
//if the selected node is a "class" attribute, add the className to it
case $node->nodeType == XML_ATTRIBUTE_NODE && $node->nodeName == 'class':
$this->addClass ($query, $value); break;
//if the selected node is any other element attribute, set its value
case $node->nodeType == XML_ATTRIBUTE_NODE:
$node->nodeValue = htmlspecialchars ($value, ENT_QUOTES); break;
//if the text is to be inserted as HTML that will be included into the output
case $asHTML:
$frag = $node->ownerDocument->createDocumentFragment ();
//if the HTML string is not valid XML, it won’t work!
$frag->appendXML (self::html_entity_decode ($value));
$node->nodeValue = '';
$node->appendChild ($frag);
break;
//otherwise, encode the text to display as-is
default:
$node->nodeValue = htmlspecialchars ($value, ENT_NOQUOTES);
}
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 () {
$this->DOMNode = $this->template->cloneNode (true);
//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
return $this;
}
}
?>