Completed
Push — master ( bdcfec...34ff96 )
by Josh
13:42
created

PHP::hasNonNullValues()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
ccs 5
cts 5
cp 1
cc 3
eloc 5
nc 3
nop 1
crap 3
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2017 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Renderers;
9
10
use DOMNode;
11
use DOMXPath;
12
use RuntimeException;
13
use s9e\TextFormatter\Renderer;
14
use s9e\TextFormatter\Utils\XPath;
15
16
abstract class PHP extends Renderer
17
{
18
	/**
19
	* @var array[] List of dictionaries used by the Quick renderer [[attrName => attrValue]]
20
	*/
21
	protected $attributes;
22
23
	/**
24
	* @var array Dictionary of replacements used by the Quick renderer [id => [match, replace]]
25
	*/
26
	protected $dynamic;
27
28
	/**
29
	* @var bool Whether to enable the Quick renderer
30
	*/
31
	public $enableQuickRenderer = false;
32
33
	/**
34
	* @var string Renderer's output
35
	*/
36
	protected $out;
37
38
	/**
39
	* @var string Regexp that matches XML elements to be rendered by the quick renderer
40
	*/
41
	protected $quickRegexp = '((?!))';
42
43
	/**
44
	* @var string Regexp that matches nodes that SHOULD NOT be rendered by the quick renderer
45
	*/
46
	protected $quickRenderingTest = '(<[!?])';
47
48
	/**
49
	* @var array Dictionary of static replacements used by the Quick renderer [id => replacement]
50
	*/
51
	protected $static;
52
53
	/**
54
	* @var DOMXPath XPath object used to query the document being rendered
55
	*/
56
	protected $xpath;
57
58
	/**
59
	* Render given DOMNode
60
	*
61
	* @param  DOMNode $node
62
	* @return void
63
	*/
64
	abstract protected function renderNode(DOMNode $node);
65
66 1
	public function __sleep()
67
	{
68 1
		return ['enableQuickRenderer', 'params'];
69
	}
70
71
	/**
72
	* Render the content of given node
73
	*
74
	* Matches the behaviour of an xsl:apply-templates element
75
	*
76
	* @param  DOMNode $root  Context node
77
	* @param  string  $query XPath query used to filter which child nodes to render
78
	* @return void
79
	*/
80 350
	protected function at(DOMNode $root, $query = null)
81
	{
82 350
		if ($root->nodeType === XML_TEXT_NODE)
83
		{
84
			// Text nodes are outputted directly
85 189
			$this->out .= htmlspecialchars($root->textContent,0);
86
		}
87
		else
88
		{
89 350
			$nodes = (isset($query)) ? $this->xpath->query($query, $root) : $root->childNodes;
90 350
			foreach ($nodes as $node)
91
			{
92 350
				$this->renderNode($node);
93
			}
94
		}
95 350
	}
96
97
	/**
98
	* Test whether given XML can be rendered with the Quick renderer
99
	*
100
	* @param  string $xml
101
	* @return bool
102
	*/
103 764
	protected function canQuickRender($xml)
104
	{
105 764
		return ($this->enableQuickRenderer && !preg_match($this->quickRenderingTest, $xml));
106
	}
107
108
	/**
109
	* Return a parameter's value as an XPath expression
110
	*
111
	* @param  string $paramName
112
	* @return string
113
	*/
114 2
	protected function getParamAsXPath($paramName)
115
	{
116 2
		return (isset($this->params[$paramName])) ? XPath::export($this->params[$paramName]) : "''";
117
	}
118
119
	/**
120
	* Extract the text content from given XML
121
	*
122
	* NOTE: numeric character entities are decoded beforehand, we don't need to decode them here
123
	*
124
	* @param  string $xml Original XML
125
	* @return string      Text content, with special characters decoded
126
	*/
127 24
	protected function getQuickTextContent($xml)
128
	{
129 24
		return htmlspecialchars_decode(strip_tags($xml));
130
	}
131
132
	/**
133
	* Test whether given array has any non-null values
134
	*
135
	* @param  array $array
136
	* @return bool
137
	*/
138 4
	protected function hasNonNullValues(array $array)
139
	{
140 4
		foreach ($array as $v)
141
		{
142 3
			if (isset($v))
143
			{
144 3
				return true;
145
			}
146
		}
147
148 2
		return false;
149
	}
150
151
	/**
152
	* Capture and return the attributes of an XML element
153
	*
154
	* NOTE: XML character entities are left as-is
155
	*
156
	* @param  string $xml Element in XML form
157
	* @return array       Dictionary of [attrName => attrValue]
158
	*/
159 262
	protected function matchAttributes($xml)
160
	{
161 262
		if (strpos($xml, '="') === false)
162
		{
163 110
			return [];
164
		}
165
166
		// Match all name-value pairs until the first right bracket
167 193
		preg_match_all('(([^ =]++)="([^"]*))S', substr($xml, 0, strpos($xml, '>')), $m);
168
169 193
		return array_combine($m[1], $m[2]);
170
	}
171
172
	/**
173
	* Render an intermediate representation using the Quick renderer
174
	*
175
	* @param  string $xml Intermediate representation
176
	* @return string
177
	*/
178 409
	protected function renderQuick($xml)
179
	{
180 409
		$this->attributes = [];
181 409
		$xml = $this->decodeSMP($xml);
182 409
		$html = preg_replace_callback(
183 409
			$this->quickRegexp,
184 409
			[$this, 'renderQuickCallback'],
185 409
			preg_replace(
186 409
				'(<[eis]>[^<]*</[eis]>)',
187 409
				'',
188 409
				substr($xml, 1 + strpos($xml, '>'), -4)
189
			)
190
		);
191
192 408
		return str_replace('<br/>', '<br>', $html);
193
	}
194
195
	/**
196
	* Render a string matched by the Quick renderer
197
	*
198
	* This stub should be overwritten by generated renderers
199
	*
200
	* @param  string[] $m
201
	* @return string
202
	*/
203 405
	protected function renderQuickCallback(array $m)
204
	{
205 405
		if (isset($m[3]))
206
		{
207 62
			return $this->renderQuickSelfClosingTag($m);
208
		}
209
210 405
		if (isset($m[2]))
211
		{
212
			// Single tag
213 213
			$id = $m[2];
214
		}
215
		else
216
		{
217
			// Tag pair
218 195
			$id = $m[1];
219 195
			$this->checkTagPairContent($id, $m[0]);
220
		}
221
222 404
		if (isset($this->static[$id]))
223
		{
224 166
			return $this->static[$id];
225
		}
226 347
		if (isset($this->dynamic[$id]))
227
		{
228 94
			return preg_replace($this->dynamic[$id][0], $this->dynamic[$id][1], $m[0], 1);
229
		}
230
231 262
		return $this->renderQuickTemplate($id, $m[0]);
232
	}
233
234
	/**
235
	* Ensure that a tag pair does not contain a start tag of itself
236
	*
237
	* Detects malformed matches such as <X><X></X>
238
	*
239
	* @param  string $id
240
	* @param  string $xml
241
	* @return void
242
	*/
243 195
	protected function checkTagPairContent($id, $xml)
244
	{
245 195
		if (strpos($xml, '<' . $id, 1) !== false)
246
		{
247 1
			throw new RuntimeException;
248
		}
249 194
	}
250
251
	/**
252
	* Render a self-closing tag using the Quick renderer
253
	*
254
	* @param  string[] $m
255
	* @return string
256
	*/
257 62
	protected function renderQuickSelfClosingTag(array $m)
258
	{
259 62
		unset($m[3]);
260
261 62
		$m[0] = substr($m[0], 0, -2) . '>';
262 62
		$html = $this->renderQuickCallback($m);
263
264 62
		$m[0] = '</' . $m[2] . '>';
265 62
		$m[2] = '/' . $m[2];
266 62
		$html .= $this->renderQuickCallback($m);
267
268 62
		return $html;
269
	}
270
271
	/**
272
	* Render a string matched by the Quick renderer using a generated PHP template
273
	*
274
	* This stub should be overwritten by generated renderers
275
	*
276
	* @param  integer $id  Template's index in the quick branch table
277
	* @param  string  $xml Tag's ID (tag name optionally preceded by a slash)
278
	* @return string       Rendered template
279
	*/
280
	protected function renderQuickTemplate($id, $xml)
281
	{
282
		throw new RuntimeException('Not implemented');
283
	}
284
285
	/**
286
	* {@inheritdoc}
287
	*/
288 764
	protected function renderRichText($xml)
289
	{
290
		try
291
		{
292 764
			if ($this->canQuickRender($xml))
293
			{
294 764
				return $this->renderQuick($xml);
295
			}
296
		}
297 1
		catch (RuntimeException $e)
298
		{
299
			// Do nothing
300
		}
301
302 356
		$dom         = $this->loadXML($xml);
303 350
		$this->out   = '';
304 350
		$this->xpath = new DOMXPath($dom);
305 350
		$this->at($dom->documentElement);
306 350
		$html        = $this->out;
307 350
		$this->reset();
308
309 350
		return $html;
310
	}
311
312
	/**
313
	* Reset object properties that are populated during rendering
314
	*
315
	* @return void
316
	*/
317 350
	protected function reset()
318
	{
319 350
		unset($this->attributes);
320 350
		unset($this->out);
321 350
		unset($this->xpath);
322
	}
323
}