Completed
Push — master ( f71236...1eeec1 )
by Josh
12:29
created

PHP::renderRichText()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

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