Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace Drupal\Tests\Component\Utility;
|
Chris@0
|
4
|
Chris@0
|
5 use Drupal\Component\Render\MarkupInterface;
|
Chris@0
|
6 use Drupal\Component\Render\MarkupTrait;
|
Chris@0
|
7 use Drupal\Component\Utility\Html;
|
Chris@0
|
8 use Drupal\Component\Utility\Random;
|
Chris@0
|
9 use PHPUnit\Framework\TestCase;
|
Chris@0
|
10
|
Chris@0
|
11 /**
|
Chris@0
|
12 * Tests \Drupal\Component\Utility\Html.
|
Chris@0
|
13 *
|
Chris@0
|
14 * @group Common
|
Chris@0
|
15 *
|
Chris@0
|
16 * @coversDefaultClass \Drupal\Component\Utility\Html
|
Chris@0
|
17 */
|
Chris@0
|
18 class HtmlTest extends TestCase {
|
Chris@0
|
19
|
Chris@0
|
20 /**
|
Chris@0
|
21 * {@inheritdoc}
|
Chris@0
|
22 */
|
Chris@0
|
23 protected function setUp() {
|
Chris@0
|
24 parent::setUp();
|
Chris@0
|
25
|
Chris@0
|
26 $property = new \ReflectionProperty('Drupal\Component\Utility\Html', 'seenIdsInit');
|
Chris@0
|
27 $property->setAccessible(TRUE);
|
Chris@0
|
28 $property->setValue(NULL);
|
Chris@0
|
29 }
|
Chris@0
|
30
|
Chris@0
|
31 /**
|
Chris@0
|
32 * Tests the Html::cleanCssIdentifier() method.
|
Chris@0
|
33 *
|
Chris@0
|
34 * @param string $expected
|
Chris@0
|
35 * The expected result.
|
Chris@0
|
36 * @param string $source
|
Chris@0
|
37 * The string being transformed to an ID.
|
Chris@0
|
38 * @param array|null $filter
|
Chris@0
|
39 * (optional) An array of string replacements to use on the identifier. If
|
Chris@0
|
40 * NULL, no filter will be passed and a default will be used.
|
Chris@0
|
41 *
|
Chris@0
|
42 * @dataProvider providerTestCleanCssIdentifier
|
Chris@0
|
43 *
|
Chris@0
|
44 * @covers ::cleanCssIdentifier
|
Chris@0
|
45 */
|
Chris@0
|
46 public function testCleanCssIdentifier($expected, $source, $filter = NULL) {
|
Chris@0
|
47 if ($filter !== NULL) {
|
Chris@0
|
48 $this->assertSame($expected, Html::cleanCssIdentifier($source, $filter));
|
Chris@0
|
49 }
|
Chris@0
|
50 else {
|
Chris@0
|
51 $this->assertSame($expected, Html::cleanCssIdentifier($source));
|
Chris@0
|
52 }
|
Chris@0
|
53 }
|
Chris@0
|
54
|
Chris@0
|
55 /**
|
Chris@0
|
56 * Provides test data for testCleanCssIdentifier().
|
Chris@0
|
57 *
|
Chris@0
|
58 * @return array
|
Chris@0
|
59 * Test data.
|
Chris@0
|
60 */
|
Chris@0
|
61 public function providerTestCleanCssIdentifier() {
|
Chris@0
|
62 $id1 = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789';
|
Chris@0
|
63 $id2 = '¡¢£¤¥';
|
Chris@0
|
64 $id3 = 'css__identifier__with__double__underscores';
|
Chris@0
|
65 return [
|
Chris@0
|
66 // Verify that no valid ASCII characters are stripped from the identifier.
|
Chris@0
|
67 [$id1, $id1, []],
|
Chris@0
|
68 // Verify that valid UTF-8 characters are not stripped from the identifier.
|
Chris@0
|
69 [$id2, $id2, []],
|
Chris@0
|
70 // Verify that invalid characters (including non-breaking space) are stripped from the identifier.
|
Chris@0
|
71 [$id3, $id3],
|
Chris@0
|
72 // Verify that double underscores are not stripped from the identifier.
|
Chris@0
|
73 ['invalididentifier', 'invalid !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', []],
|
Chris@0
|
74 // Verify that an identifier starting with a digit is replaced.
|
Chris@0
|
75 ['_cssidentifier', '1cssidentifier', []],
|
Chris@0
|
76 // Verify that an identifier starting with a hyphen followed by a digit is
|
Chris@0
|
77 // replaced.
|
Chris@0
|
78 ['__cssidentifier', '-1cssidentifier', []],
|
Chris@0
|
79 // Verify that an identifier starting with two hyphens is replaced.
|
Chris@0
|
80 ['__cssidentifier', '--cssidentifier', []],
|
Chris@0
|
81 // Verify that passing double underscores as a filter is processed.
|
Chris@0
|
82 ['_cssidentifier', '__cssidentifier', ['__' => '_']],
|
Chris@0
|
83 ];
|
Chris@0
|
84 }
|
Chris@0
|
85
|
Chris@0
|
86 /**
|
Chris@0
|
87 * Tests that Html::getClass() cleans the class name properly.
|
Chris@0
|
88 *
|
Chris@0
|
89 * @coversDefaultClass ::getClass
|
Chris@0
|
90 */
|
Chris@0
|
91 public function testHtmlClass() {
|
Chris@0
|
92 // Verify Drupal coding standards are enforced.
|
Chris@0
|
93 $this->assertSame('class-name--ü', Html::getClass('CLASS NAME_[Ü]'), 'Enforce Drupal coding standards.');
|
Chris@0
|
94
|
Chris@0
|
95 // Test Html::getClass() handles Drupal\Component\Render\MarkupInterface
|
Chris@0
|
96 // input.
|
Chris@0
|
97 $markup = HtmlTestMarkup::create('CLASS_FROM_OBJECT');
|
Chris@0
|
98 $this->assertSame('class-from-object', Html::getClass($markup), 'Markup object is converted to CSS class.');
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 /**
|
Chris@0
|
102 * Tests the Html::getUniqueId() method.
|
Chris@0
|
103 *
|
Chris@0
|
104 * @param string $expected
|
Chris@0
|
105 * The expected result.
|
Chris@0
|
106 * @param string $source
|
Chris@0
|
107 * The string being transformed to an ID.
|
Chris@0
|
108 * @param bool $reset
|
Chris@0
|
109 * (optional) If TRUE, reset the list of seen IDs. Defaults to FALSE.
|
Chris@0
|
110 *
|
Chris@0
|
111 * @dataProvider providerTestHtmlGetUniqueId
|
Chris@0
|
112 *
|
Chris@0
|
113 * @covers ::getUniqueId
|
Chris@0
|
114 */
|
Chris@0
|
115 public function testHtmlGetUniqueId($expected, $source, $reset = FALSE) {
|
Chris@0
|
116 if ($reset) {
|
Chris@0
|
117 Html::resetSeenIds();
|
Chris@0
|
118 }
|
Chris@0
|
119 $this->assertSame($expected, Html::getUniqueId($source));
|
Chris@0
|
120 }
|
Chris@0
|
121
|
Chris@0
|
122 /**
|
Chris@0
|
123 * Provides test data for testHtmlGetId().
|
Chris@0
|
124 *
|
Chris@0
|
125 * @return array
|
Chris@0
|
126 * Test data.
|
Chris@0
|
127 */
|
Chris@0
|
128 public function providerTestHtmlGetUniqueId() {
|
Chris@0
|
129 $id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
|
Chris@0
|
130 return [
|
Chris@0
|
131 // Verify that letters, digits, and hyphens are not stripped from the ID.
|
Chris@0
|
132 [$id, $id],
|
Chris@0
|
133 // Verify that invalid characters are stripped from the ID.
|
Chris@0
|
134 ['invalididentifier', 'invalid,./:@\\^`{Üidentifier'],
|
Chris@0
|
135 // Verify Drupal coding standards are enforced.
|
Chris@0
|
136 ['id-name-1', 'ID NAME_[1]'],
|
Chris@0
|
137 // Verify that a repeated ID is made unique.
|
Chris@0
|
138 ['test-unique-id', 'test-unique-id', TRUE],
|
Chris@0
|
139 ['test-unique-id--2', 'test-unique-id'],
|
Chris@0
|
140 ['test-unique-id--3', 'test-unique-id'],
|
Chris@0
|
141 ];
|
Chris@0
|
142 }
|
Chris@0
|
143
|
Chris@0
|
144 /**
|
Chris@0
|
145 * Tests the Html::getUniqueId() method.
|
Chris@0
|
146 *
|
Chris@0
|
147 * @param string $expected
|
Chris@0
|
148 * The expected result.
|
Chris@0
|
149 * @param string $source
|
Chris@0
|
150 * The string being transformed to an ID.
|
Chris@0
|
151 *
|
Chris@0
|
152 * @dataProvider providerTestHtmlGetUniqueIdWithAjaxIds
|
Chris@0
|
153 *
|
Chris@0
|
154 * @covers ::getUniqueId
|
Chris@0
|
155 */
|
Chris@0
|
156 public function testHtmlGetUniqueIdWithAjaxIds($expected, $source) {
|
Chris@0
|
157 Html::setIsAjax(TRUE);
|
Chris@0
|
158 $id = Html::getUniqueId($source);
|
Chris@0
|
159
|
Chris@0
|
160 // Note, we truncate two hyphens at the end.
|
Chris@0
|
161 // @see \Drupal\Component\Utility\Html::getId()
|
Chris@0
|
162 if (strpos($source, '--') !== FALSE) {
|
Chris@0
|
163 $random_suffix = substr($id, strlen($source) + 1);
|
Chris@0
|
164 }
|
Chris@0
|
165 else {
|
Chris@0
|
166 $random_suffix = substr($id, strlen($source) + 2);
|
Chris@0
|
167 }
|
Chris@0
|
168 $expected = $expected . $random_suffix;
|
Chris@0
|
169 $this->assertSame($expected, $id);
|
Chris@0
|
170 }
|
Chris@0
|
171
|
Chris@0
|
172 /**
|
Chris@0
|
173 * Provides test data for testHtmlGetId().
|
Chris@0
|
174 *
|
Chris@0
|
175 * @return array
|
Chris@0
|
176 * Test data.
|
Chris@0
|
177 */
|
Chris@0
|
178 public function providerTestHtmlGetUniqueIdWithAjaxIds() {
|
Chris@0
|
179 return [
|
Chris@0
|
180 ['test-unique-id1--', 'test-unique-id1'],
|
Chris@0
|
181 // Note, we truncate two hyphens at the end.
|
Chris@0
|
182 // @see \Drupal\Component\Utility\Html::getId()
|
Chris@0
|
183 ['test-unique-id1---', 'test-unique-id1--'],
|
Chris@0
|
184 ['test-unique-id2--', 'test-unique-id2'],
|
Chris@0
|
185 ];
|
Chris@0
|
186 }
|
Chris@0
|
187
|
Chris@0
|
188 /**
|
Chris@0
|
189 * Tests the Html::getUniqueId() method.
|
Chris@0
|
190 *
|
Chris@0
|
191 * @param string $expected
|
Chris@0
|
192 * The expected result.
|
Chris@0
|
193 * @param string $source
|
Chris@0
|
194 * The string being transformed to an ID.
|
Chris@0
|
195 *
|
Chris@0
|
196 * @dataProvider providerTestHtmlGetId
|
Chris@0
|
197 *
|
Chris@0
|
198 * @covers ::getId
|
Chris@0
|
199 */
|
Chris@0
|
200 public function testHtmlGetId($expected, $source) {
|
Chris@0
|
201 Html::setIsAjax(FALSE);
|
Chris@0
|
202 $this->assertSame($expected, Html::getId($source));
|
Chris@0
|
203 }
|
Chris@0
|
204
|
Chris@0
|
205 /**
|
Chris@0
|
206 * Provides test data for testHtmlGetId().
|
Chris@0
|
207 *
|
Chris@0
|
208 * @return array
|
Chris@0
|
209 * Test data.
|
Chris@0
|
210 */
|
Chris@0
|
211 public function providerTestHtmlGetId() {
|
Chris@0
|
212 $id = 'abcdefghijklmnopqrstuvwxyz-0123456789';
|
Chris@0
|
213 return [
|
Chris@0
|
214 // Verify that letters, digits, and hyphens are not stripped from the ID.
|
Chris@0
|
215 [$id, $id],
|
Chris@0
|
216 // Verify that invalid characters are stripped from the ID.
|
Chris@0
|
217 ['invalididentifier', 'invalid,./:@\\^`{Üidentifier'],
|
Chris@0
|
218 // Verify Drupal coding standards are enforced.
|
Chris@0
|
219 ['id-name-1', 'ID NAME_[1]'],
|
Chris@0
|
220 // Verify that a repeated ID is made unique.
|
Chris@0
|
221 ['test-unique-id', 'test-unique-id'],
|
Chris@0
|
222 ['test-unique-id', 'test-unique-id'],
|
Chris@0
|
223 ];
|
Chris@0
|
224 }
|
Chris@0
|
225
|
Chris@0
|
226 /**
|
Chris@0
|
227 * Tests Html::decodeEntities().
|
Chris@0
|
228 *
|
Chris@0
|
229 * @dataProvider providerDecodeEntities
|
Chris@0
|
230 * @covers ::decodeEntities
|
Chris@0
|
231 */
|
Chris@0
|
232 public function testDecodeEntities($text, $expected) {
|
Chris@0
|
233 $this->assertEquals($expected, Html::decodeEntities($text));
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 /**
|
Chris@0
|
237 * Data provider for testDecodeEntities().
|
Chris@0
|
238 *
|
Chris@0
|
239 * @see testDecodeEntities()
|
Chris@0
|
240 */
|
Chris@0
|
241 public function providerDecodeEntities() {
|
Chris@0
|
242 return [
|
Chris@0
|
243 ['Drupal', 'Drupal'],
|
Chris@0
|
244 ['<script>', '<script>'],
|
Chris@0
|
245 ['<script>', '<script>'],
|
Chris@0
|
246 ['<script>', '<script>'],
|
Chris@0
|
247 ['&lt;script&gt;', '<script>'],
|
Chris@0
|
248 ['"', '"'],
|
Chris@0
|
249 ['"', '"'],
|
Chris@0
|
250 ['&#34;', '"'],
|
Chris@0
|
251 ['"', '"'],
|
Chris@0
|
252 ['&quot;', '"'],
|
Chris@0
|
253 ["'", "'"],
|
Chris@0
|
254 [''', "'"],
|
Chris@0
|
255 ['&#39;', '''],
|
Chris@0
|
256 ['©', '©'],
|
Chris@0
|
257 ['©', '©'],
|
Chris@0
|
258 ['©', '©'],
|
Chris@0
|
259 ['→', '→'],
|
Chris@0
|
260 ['→', '→'],
|
Chris@0
|
261 ['➼', '➼'],
|
Chris@0
|
262 ['➼', '➼'],
|
Chris@0
|
263 ['€', '€'],
|
Chris@0
|
264 ];
|
Chris@0
|
265 }
|
Chris@0
|
266
|
Chris@0
|
267 /**
|
Chris@0
|
268 * Tests Html::escape().
|
Chris@0
|
269 *
|
Chris@0
|
270 * @dataProvider providerEscape
|
Chris@0
|
271 * @covers ::escape
|
Chris@0
|
272 */
|
Chris@0
|
273 public function testEscape($expected, $text) {
|
Chris@0
|
274 $this->assertEquals($expected, Html::escape($text));
|
Chris@0
|
275 }
|
Chris@0
|
276
|
Chris@0
|
277 /**
|
Chris@0
|
278 * Data provider for testEscape().
|
Chris@0
|
279 *
|
Chris@0
|
280 * @see testEscape()
|
Chris@0
|
281 */
|
Chris@0
|
282 public function providerEscape() {
|
Chris@0
|
283 return [
|
Chris@0
|
284 ['Drupal', 'Drupal'],
|
Chris@0
|
285 ['<script>', '<script>'],
|
Chris@0
|
286 ['&lt;script&gt;', '<script>'],
|
Chris@0
|
287 ['&#34;', '"'],
|
Chris@0
|
288 ['"', '"'],
|
Chris@0
|
289 ['&quot;', '"'],
|
Chris@0
|
290 [''', "'"],
|
Chris@0
|
291 ['&#039;', '''],
|
Chris@0
|
292 ['©', '©'],
|
Chris@0
|
293 ['→', '→'],
|
Chris@0
|
294 ['➼', '➼'],
|
Chris@0
|
295 ['€', '€'],
|
Chris@0
|
296 ['Drup�al', "Drup\x80al"],
|
Chris@0
|
297 ];
|
Chris@0
|
298 }
|
Chris@0
|
299
|
Chris@0
|
300 /**
|
Chris@0
|
301 * Tests relationship between escaping and decoding HTML entities.
|
Chris@0
|
302 *
|
Chris@0
|
303 * @covers ::decodeEntities
|
Chris@0
|
304 * @covers ::escape
|
Chris@0
|
305 */
|
Chris@0
|
306 public function testDecodeEntitiesAndEscape() {
|
Chris@0
|
307 $string = "<em>répété</em>";
|
Chris@0
|
308 $escaped = Html::escape($string);
|
Chris@0
|
309 $this->assertSame('<em>répét&eacute;</em>', $escaped);
|
Chris@0
|
310 $decoded = Html::decodeEntities($escaped);
|
Chris@0
|
311 $this->assertSame('<em>répété</em>', $decoded);
|
Chris@0
|
312 $decoded = Html::decodeEntities($decoded);
|
Chris@0
|
313 $this->assertSame('<em>répété</em>', $decoded);
|
Chris@0
|
314 $escaped = Html::escape($decoded);
|
Chris@0
|
315 $this->assertSame('<em>répété</em>', $escaped);
|
Chris@0
|
316 }
|
Chris@0
|
317
|
Chris@0
|
318 /**
|
Chris@0
|
319 * Tests Html::serialize().
|
Chris@0
|
320 *
|
Chris@0
|
321 * Resolves an issue by where an empty DOMDocument object sent to serialization would
|
Chris@0
|
322 * cause errors in getElementsByTagName() in the serialization function.
|
Chris@0
|
323 *
|
Chris@0
|
324 * @covers ::serialize
|
Chris@0
|
325 */
|
Chris@0
|
326 public function testSerialize() {
|
Chris@0
|
327 $document = new \DOMDocument();
|
Chris@0
|
328 $result = Html::serialize($document);
|
Chris@0
|
329 $this->assertSame('', $result);
|
Chris@0
|
330 }
|
Chris@0
|
331
|
Chris@0
|
332 /**
|
Chris@0
|
333 * @covers ::transformRootRelativeUrlsToAbsolute
|
Chris@0
|
334 * @dataProvider providerTestTransformRootRelativeUrlsToAbsolute
|
Chris@0
|
335 */
|
Chris@0
|
336 public function testTransformRootRelativeUrlsToAbsolute($html, $scheme_and_host, $expected_html) {
|
Chris@0
|
337 $this->assertSame($expected_html ?: $html, Html::transformRootRelativeUrlsToAbsolute($html, $scheme_and_host));
|
Chris@0
|
338 }
|
Chris@0
|
339
|
Chris@0
|
340 /**
|
Chris@0
|
341 * @covers ::transformRootRelativeUrlsToAbsolute
|
Chris@0
|
342 * @dataProvider providerTestTransformRootRelativeUrlsToAbsoluteAssertion
|
Chris@0
|
343 */
|
Chris@0
|
344 public function testTransformRootRelativeUrlsToAbsoluteAssertion($scheme_and_host) {
|
Chris@0
|
345 $this->setExpectedException(\AssertionError::class);
|
Chris@0
|
346 Html::transformRootRelativeUrlsToAbsolute('', $scheme_and_host);
|
Chris@0
|
347 }
|
Chris@0
|
348
|
Chris@0
|
349 /**
|
Chris@0
|
350 * Provides test data for testTransformRootRelativeUrlsToAbsolute().
|
Chris@0
|
351 *
|
Chris@0
|
352 * @return array
|
Chris@0
|
353 * Test data.
|
Chris@0
|
354 */
|
Chris@0
|
355 public function providerTestTransformRootRelativeUrlsToAbsolute() {
|
Chris@0
|
356 $data = [];
|
Chris@0
|
357
|
Chris@0
|
358 // Random generator.
|
Chris@0
|
359 $random = new Random();
|
Chris@0
|
360
|
Chris@0
|
361 // One random tag name.
|
Chris@0
|
362 $tag_name = strtolower($random->name(8, TRUE));
|
Chris@0
|
363
|
Chris@0
|
364 // A site installed either in the root of a domain or a subdirectory.
|
Chris@0
|
365 $base_paths = ['/', '/subdir/' . $random->name(8, TRUE) . '/'];
|
Chris@0
|
366
|
Chris@0
|
367 foreach ($base_paths as $base_path) {
|
Chris@0
|
368 // The only attribute that has more than just a URL as its value, is
|
Chris@0
|
369 // 'srcset', so special-case it.
|
Chris@0
|
370 $data += [
|
Chris@0
|
371 "$tag_name, srcset, $base_path: root-relative" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, {$base_path}root-relative 300w\">root-relative test</$tag_name>", 'http://example.com', "<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, http://example.com{$base_path}root-relative 300w\">root-relative test</$tag_name>"],
|
Chris@0
|
372 "$tag_name, srcset, $base_path: protocol-relative" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, //example.com{$base_path}protocol-relative 300w\">protocol-relative test</$tag_name>", 'http://example.com', FALSE],
|
Chris@0
|
373 "$tag_name, srcset, $base_path: absolute" => ["<$tag_name srcset=\"http://example.com{$base_path}already-absolute 200w, http://example.com{$base_path}absolute 300w\">absolute test</$tag_name>", 'http://example.com', FALSE],
|
Chris@0
|
374 ];
|
Chris@0
|
375
|
Chris@0
|
376 foreach (['href', 'poster', 'src', 'cite', 'data', 'action', 'formaction', 'about'] as $attribute) {
|
Chris@0
|
377 $data += [
|
Chris@0
|
378 "$tag_name, $attribute, $base_path: root-relative" => ["<$tag_name $attribute=\"{$base_path}root-relative\">root-relative test</$tag_name>", 'http://example.com', "<$tag_name $attribute=\"http://example.com{$base_path}root-relative\">root-relative test</$tag_name>"],
|
Chris@0
|
379 "$tag_name, $attribute, $base_path: protocol-relative" => ["<$tag_name $attribute=\"//example.com{$base_path}protocol-relative\">protocol-relative test</$tag_name>", 'http://example.com', FALSE],
|
Chris@0
|
380 "$tag_name, $attribute, $base_path: absolute" => ["<$tag_name $attribute=\"http://example.com{$base_path}absolute\">absolute test</$tag_name>", 'http://example.com', FALSE],
|
Chris@0
|
381 ];
|
Chris@0
|
382 }
|
Chris@0
|
383 }
|
Chris@0
|
384
|
Chris@0
|
385 return $data;
|
Chris@0
|
386 }
|
Chris@0
|
387
|
Chris@0
|
388 /**
|
Chris@0
|
389 * Provides test data for testTransformRootRelativeUrlsToAbsoluteAssertion().
|
Chris@0
|
390 *
|
Chris@0
|
391 * @return array
|
Chris@0
|
392 * Test data.
|
Chris@0
|
393 */
|
Chris@0
|
394 public function providerTestTransformRootRelativeUrlsToAbsoluteAssertion() {
|
Chris@0
|
395 return [
|
Chris@0
|
396 'only relative path' => ['llama'],
|
Chris@0
|
397 'only root-relative path' => ['/llama'],
|
Chris@0
|
398 'host and path' => ['example.com/llama'],
|
Chris@0
|
399 'scheme, host and path' => ['http://example.com/llama'],
|
Chris@0
|
400 ];
|
Chris@0
|
401 }
|
Chris@0
|
402
|
Chris@0
|
403 }
|
Chris@0
|
404
|
Chris@0
|
405 /**
|
Chris@0
|
406 * Marks an object's __toString() method as returning markup.
|
Chris@0
|
407 */
|
Chris@0
|
408 class HtmlTestMarkup implements MarkupInterface {
|
Chris@0
|
409 use MarkupTrait;
|
Chris@0
|
410
|
Chris@0
|
411 }
|