Passed
Push — master ( ffccc0...1e1bf2 )
by Josh
03:10
created

Renderer::restoreLocale()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2020 The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter;
9
10
use DOMDocument;
11
use InvalidArgumentException;
12
13
abstract class Renderer
14
{
15
	/**
16
	* @var array Associative array of [paramName => paramValue]
17
	*/
18
	protected $params = [];
19
20
	/**
21
	* @var mixed Saved locale
22
	*/
23
	protected $savedLocale = 0;
24
25
	/**
26
	* Create a return a new DOMDocument loaded with given XML
27
	*
28
	* @param  string      $xml Source XML
29
	* @return DOMDocument
30
	*/
31 29
	protected function loadXML($xml)
32
	{
33 29
		$this->checkUnsupported($xml);
34
35
		// Activate small nodes allocation and relax LibXML's hardcoded limits if applicable. Limits
36
		// on tags can be set during configuration
37 21
		$flags = (LIBXML_VERSION >= 20700) ? LIBXML_COMPACT | LIBXML_PARSEHUGE : 0;
38
39 21
		$useErrors = libxml_use_internal_errors(true);
40 21
		$dom       = new DOMDocument;
41 21
		$success   = $dom->loadXML($xml, $flags);
42 21
		libxml_use_internal_errors($useErrors);
43
44 21
		if (!$success)
45
		{
46 3
			throw new InvalidArgumentException('Cannot load XML: ' . libxml_get_last_error()->message);
47
		}
48
49 18
		return $dom;
50
	}
51
52
	/**
53
	* Render an intermediate representation
54
	*
55
	* @param  string $xml Intermediate representation
56
	* @return string      Rendered result
57
	*/
58 46
	public function render($xml)
59
	{
60 46
		if (substr($xml, 0, 3) === '<t>' && substr($xml, -4) === '</t>')
61
		{
62 10
			return $this->renderPlainText($xml);
63
		}
64
		else
65
		{
66 36
			return $this->renderRichText(preg_replace('(<[eis]>[^<]*</[eis]>)', '', $xml));
67
		}
68
	}
69
70
	/**
71
	* Render an intermediate representation of plain text
72
	*
73
	* @param  string $xml Intermediate representation
74
	* @return string      Rendered result
75
	*/
76 10
	protected function renderPlainText($xml)
77
	{
78
		// Remove the <t> and </t> tags
79 10
		$html = substr($xml, 3, -4);
80
81
		// Replace all <br/> with <br>
82 10
		$html = str_replace('<br/>', '<br>', $html);
83
84
		// Decode encoded characters from the Supplementary Multilingual Plane
85 10
		$html = $this->decodeSMP($html);
86
87 10
		return $html;
88
	}
89
90
	/**
91
	* Render an intermediate representation of rich text
92
	*
93
	* @param  string $xml Intermediate representation
94
	* @return string      Rendered result
95
	*/
96
	abstract protected function renderRichText($xml);
97
98
	/**
99
	* Get the value of a parameter
100
	*
101
	* @param  string $paramName
102
	* @return string
103
	*/
104 6
	public function getParameter($paramName)
105
	{
106 6
		return (isset($this->params[$paramName])) ? $this->params[$paramName] : '';
107
	}
108
109
	/**
110
	* Get the values of all parameters
111
	*
112
	* @return array Associative array of parameter names and values
113
	*/
114 5
	public function getParameters()
115
	{
116 5
		return $this->params;
117
	}
118
119
	/**
120
	* Set the value of a parameter from the stylesheet
121
	*
122
	* @param  string $paramName  Parameter name
123
	* @param  mixed  $paramValue Parameter's value
124
	* @return void
125
	*/
126 6
	public function setParameter($paramName, $paramValue)
127
	{
128 6
		$this->params[$paramName] = (string) $paramValue;
129
	}
130
131
	/**
132
	* Set the values of several parameters from the stylesheet
133
	*
134
	* @param  string $params Associative array of [parameter name => parameter value]
135
	* @return void
136
	*/
137 3
	public function setParameters(array $params)
138
	{
139 3
		foreach ($params as $paramName => $paramValue)
140
		{
141 3
			$this->setParameter($paramName, $paramValue);
142
		}
143
	}
144
145
	/**
146
	* Test for the presence of unsupported XML and throw an exception if found
147
	*
148
	* @param  string $xml XML
149
	* @return void
150
	*/
151 29
	protected function checkUnsupported($xml)
152
	{
153 29
		if (preg_match('((?<=<)[!?])', $xml, $m))
154
		{
155
			$errors = [
156 8
				'!' => 'DTDs, CDATA nodes and comments are not allowed',
157
				'?' => 'Processing instructions are not allowed'
158
			];
159
160 8
			throw new InvalidArgumentException($errors[$m[0]]);
161
		}
162
	}
163
164
	/**
165
	* Decode encoded characters from the Supplementary Multilingual Plane
166
	*
167
	* @param  string $str Encoded string
168
	* @return string      Decoded string
169
	*/
170 21
	protected function decodeSMP($str)
171
	{
172 21
		if (strpos($str, '&#') === false)
173
		{
174 11
			return $str;
175
		}
176
177 10
		return preg_replace_callback('(&#(?:x[0-9A-Fa-f]+|[0-9]+);)', __CLASS__ . '::decodeEntity', $str);
178
	}
179
180
	/**
181
	* Decode a matched SGML entity
182
	*
183
	* @param  string[] $m Captures from PCRE
184
	* @return string      Decoded entity
185
	*/
186 10
	protected static function decodeEntity(array $m)
187
	{
188 10
		return htmlspecialchars(html_entity_decode($m[0], ENT_QUOTES, 'UTF-8'), ENT_COMPAT);
189
	}
190
191
	/**
192
	* Restore the original locale
193
	*/
194 25
	protected function restoreLocale(): void
195
	{
196 25
		if ($this->savedLocale !== 'C')
197
		{
198 14
			setlocale(LC_NUMERIC, $this->savedLocale);
199
		}
200
	}
201
202
	/**
203
	* Temporarily set the locale to C
204
	*/
205 30
	protected function setLocale(): void
206
	{
207 30
		$this->savedLocale = setlocale(LC_NUMERIC, 0);
208 30
		if ($this->savedLocale !== 'C')
209
		{
210 16
			setlocale(LC_NUMERIC, 'C');
211
		}
212
	}
213
}