diff --git a/administrator/language/en-GB/en-GB.plg_system_microdata.ini b/administrator/language/en-GB/en-GB.plg_system_microdata.ini new file mode 100644 index 0000000000000..4afb586aaf1c0 --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_microdata.ini @@ -0,0 +1,9 @@ +; Joomla! Project +; Copyright (C) 2005 - 2014 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 - No BOM + +PLG_SYSTEM_MICRODATA="System - Microdata" +PLG_SYSTEM_MICRODATA_FIELD_SUFFIX_DESCRIPTION="The suffix to search for when parsing the data-* HTML5 attribute" +PLG_SYSTEM_MICRODATA_FIELD_SUFFIX_LABEL="The data-* suffix" +PLG_SYSTEM_MICRODATA_XML_DESCRIPTION="System Plugin for parsing the HTML markup and convert the 'data-sd' HTML5 attributes in Microdata semantics." \ No newline at end of file diff --git a/administrator/language/en-GB/en-GB.plg_system_microdata.sys.ini b/administrator/language/en-GB/en-GB.plg_system_microdata.sys.ini new file mode 100644 index 0000000000000..a12b5ca36163d --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_microdata.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; Copyright (C) 2005 - 2014 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 - No BOM + +PLG_SYSTEM_MICRODATA="System - Microdata" +PLG_SYSTEM_MICRODATA_XML_DESCRIPTION="System Plugin for parsing the HTML markup and convert the 'data-sd' HTML5 attributes in Microdata semantics." \ No newline at end of file diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql index 0ad451a575d3a..f6d7a12ff7835 100644 --- a/installation/sql/mysql/joomla.sql +++ b/installation/sql/mysql/joomla.sql @@ -610,6 +610,7 @@ INSERT INTO `#__extensions` (`extension_id`, `name`, `type`, `element`, `folder` (449, 'plg_authentication_cookie', 'plugin', 'cookie', 'authentication', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), (450, 'plg_twofactorauth_yubikey', 'plugin', 'yubikey', 'twofactorauth', 0, 0, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), (451, 'plg_search_tags', 'plugin', 'tags', 'search', 0, 1, 1, 0, '', '{"search_limit":"50","show_tagged_items":"1"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), +(452, 'plg_search_microdata', 'plugin', 'microdata', 'system', 0, 1, 1, 0, '', '{"suffix":"sd"}', '', '', 0, '0000-00-00 00:00:00', 100, 0), (503, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (504, 'hathor', 'template', 'hathor', '', 1, 1, 1, 0, '', '{"showSiteName":"0","colourChoice":"0","boldText":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (506, 'protostar', 'template', 'protostar', '', 0, 1, 1, 0, '', '{"templateColor":"","logoFile":"","googleFont":"1","googleFontName":"Open+Sans","fluidContainer":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), diff --git a/installation/sql/postgresql/joomla.sql b/installation/sql/postgresql/joomla.sql index 011b918313835..30fc480647e2e 100644 --- a/installation/sql/postgresql/joomla.sql +++ b/installation/sql/postgresql/joomla.sql @@ -611,6 +611,7 @@ INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder" (449, 'plg_authentication_cookie', 'plugin', 'cookie', 'authentication', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (450, 'plg_twofactorauth_yubikey', 'plugin', 'yubikey', 'twofactorauth', 0, 0, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (451, 'plg_search_tags', 'plugin', 'tags', 'search', 0, 1, 1, 0, '', '{"search_limit":"50","show_tagged_items":"1"}', '', '', 0, '1970-01-01 00:00:00', 0, 0); +(452, 'plg_search_microdata', 'plugin', 'microdata', 'system', 0, 1, 1, 0, '', '{"suffix":"sd"}', '', '', 0, '0000-00-00 00:00:00', 100, 0); -- Templates INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES diff --git a/installation/sql/sqlazure/joomla.sql b/installation/sql/sqlazure/joomla.sql index aae100f979ab0..f12b943c6348b 100644 --- a/installation/sql/sqlazure/joomla.sql +++ b/installation/sql/sqlazure/joomla.sql @@ -1012,7 +1012,8 @@ UNION ALL SELECT 450, 'plg_twofactorauth_yubikey', 'plugin', 'yubikey', 'twofactorauth', 0, 0, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0 UNION ALL SELECT 451, 'plg_search_tags', 'plugin', 'tags', 'search', 0, 1, 1, 0, '', '{"search_limit":"50","show_tagged_items":"1"}', '', '', 0, '1900-01-01 00:00:00', 0, 0; - +UNION ALL +SELECT 452, 'plg_search_microdata', 'plugin', 'microdata', 'system', 0, 1, 1, 0, '', '{"suffix":"sd"}', '', '', 0, '0000-00-00 00:00:00', 100, 0; INSERT [#__extensions] ([extension_id], [name], [type], [element], [folder], [client_id], [enabled], [access], [protected], [manifest_cache], [params], [custom_data], [system_data], [checked_out], [checked_out_time], [ordering], [state]) SELECT 503, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '1900-01-01 00:00:00', 0, 0 diff --git a/libraries/joomla/microdata/parser.php b/libraries/joomla/microdata/parser.php new file mode 100644 index 0000000000000..6730a4512f3fa --- /dev/null +++ b/libraries/joomla/microdata/parser.php @@ -0,0 +1,437 @@ +suffix($suffix); + } + + $this->handler = new JMicrodata; + } + + /** + * Return the $handler, which is an instance of JMicrodata + * + * @return JMicrodata + * + * @since 3.3 + */ + public function getHandler() + { + return $this->handler; + } + + /** + * Setup the $suffix to search for when parsing the data-* HTML5 attribute + * + * @param mixed $suffix The suffix + * + * @return JMicrodataParser + * + * @since 3.3 + */ + public function suffix($suffix) + { + if (is_array($suffix)) + { + while ($string = array_pop($suffix)) + { + $this->addSuffix($string); + } + + return $this; + } + + $this->addSuffix($suffix); + + return $this; + } + + /** + * Add a new $suffix to search for when parsing the data-* HTML5 attribute + * + * @param string $string The suffix + * + * @return void + * + * @since 3.3 + */ + protected function addSuffix($string) + { + $string = trim(strtolower((string) $string)); + + // Avoid adding a duplicate suffix, also the suffix must be at least one character long + if (array_search($string, $this->suffix) || empty($string)) + { + return; + } + + // Add the new suffix + array_push($this->suffix, $string); + } + + /** + * Remove a $suffix entry + * + * @param string $string The suffix + * + * @return JMicrodataParser + * + * @since 3.3 + */ + public function removeSuffix($string) + { + $string = strtolower((string) $string); + + // Search and remove the suffix + unset( + $this->suffix[array_search($string, $this->suffix)] + ); + + return $this; + } + + /** + * Return the current $suffix + * + * @return string + * + * @since 3.3 + */ + public function getSuffix() + { + return $this->suffix; + } + + /** + * Parse the unit param that will be used to setup the JMicrodata class, + * e.g. giving the following: $string = 'Type.property.EType'; + * will return an array: + * array( + * 'type' => 'Type, + * 'property' => 'property' + * 'expectedType => 'EType' + * ); + * + * @param string $string The string to parse + * + * @return array + */ + protected static function parseParam($string) + { + // The default array + $params = array( + 'type' => null, + 'property' => null, + 'expectedType' => null + ); + + // Sanitize the $string and parse + $string = explode('.', trim((string) $string)); + + // If no matches found return the default array + if (empty($string[0])) + { + return $params; + } + + // If the first letter is uppercase, then the param string could be 'Type.property.EType', otherwise it should be the 'property.EType' + if (ctype_upper($string[0]{0})) + { + $params['type'] = $string[0]; + + // If the first letter is lowercase, then it should be the property, otherwise return + if (count($string) > 1 && !empty($string[1]) && ctype_lower($string[1]{0})) + { + $params['property'] = $string[1]; + + // If the first letter is uppercase, then it should be expected Type, otherwise return + if (count($string) > 2 && !empty($string[2]) && ctype_upper($string[2]{0})) + { + $params['expectedType'] = $string[2]; + } + } + } + else + { + $params['property'] = $string[0]; + + // If the first letter is uppercase, then it should be the expectedType + if (count($string) > 1 && !empty($string[1]) && ctype_upper($string[1]{0})) + { + $params['expectedType'] = $string[1]; + } + } + + return $params; + } + + /** + * Parse the params that will be used to setup the JMicrodata class, + * e.g giving the following: $string ='Type Type.property.EType ... FType.fProperty gProperty.EType sProperty'; + * will return an array: + * array( + * 'setType' => 'Type', + * 'fallbacks' => array( + * 'specialized' => array( + * 'Type' => array('property' => 'EType'), + * 'FType' => array('fproperty' => null) + * ... + * ), + * 'global' => array( + * ... + * 'gProperty' => 'EType', + * 'sProperty' => null + * ) + * ) + * ); + * + * @param string $string The string to parse + * + * @return array + */ + protected static function parseParams($string) + { + // The default array + $params = array( + 'setType' => null, + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array() + ) + ); + + // Sanitize the $string, remove single and multiple whitespaces + $string = trim(preg_replace('/\s+/', ' ', (string) $string)); + + // Break the strings in small param chunks + $string = explode(' ', $string); + + // Parse the small param chunks + foreach ($string as $match) + { + $tmp = self::parseParam($match); + $type = $tmp['type']; + $property = $tmp['property']; + $expectedType = $tmp['expectedType']; + + // If a 'type' is available and there is no 'property', then it should be a 'setType' + if ($type && !$property && !$params['setType']) + { + $params['setType'] = $type; + } + + // If a 'property' is available and there is no 'type', then it should be a 'global' fallback + if (!$type && $property) + { + $params['fallbacks']['global'][$property] = $expectedType; + } + + // If both 'type' and 'property' is available, then it should be a 'specialized' fallback + if ($type && $property && !array_key_exists($type, $params['fallbacks']['specialized'])) + { + $params['fallbacks']['specialized'][$type] = array($property => $expectedType); + } + } + + return $params; + } + + /** + * Generate the Microdata semantics + * + * @param array $params The params used to setup the JMicrodata library + * + * @return string + */ + protected function display($params) + { + $html = ''; + $setType = $params['setType']; + + // Specialized fallbacks + $sFallbacks = $params['fallbacks']['specialized']; + + // Global fallbacks + $gFallbacks = $params['fallbacks']['global']; + + // Set the current Type if available + if ($setType) + { + $this->handler->setType($setType); + } + + // If no properties available and there is a 'setType', return and display the scope + if ($setType && !$sFallbacks && !$gFallbacks) + { + return $this->handler->displayScope(); + } + + // Get the current Type + $currentType = $this->handler->getType(); + + // Check if there is an available 'specialized' fallback property for the current Type + if ($sFallbacks && array_key_exists($currentType, $sFallbacks)) + { + $property = key($sFallbacks[$currentType]); + $expectedType = $sFallbacks[$currentType][$property]; + + $html .= $this->handler->property($property)->display('inline'); + + // Check if an expected Type is available and it is valid + if ($expectedType + && in_array($expectedType, JMicrodata::getExpectedTypes($currentType, $property))) + { + // Update the current Type + $this->handler->setType($expectedType); + + // Display the scope + $html .= ' ' . $this->handler->displayScope(); + } + + return $html; + } + + // Check if there is an available 'global' fallback property for the current Type + if ($gFallbacks) + { + foreach ($gFallbacks as $property => $expectedType) + { + // Check if the property is available in the current Type + if (JMicrodata::isPropertyInType($currentType, $property)) + { + $html .= $this->handler->property($property)->display('inline'); + + // Check if an expected Type is available + if ($expectedType + && in_array($expectedType, JMicrodata::getExpectedTypes($currentType, $property))) + { + // Update the current Type + $this->handler->setType($expectedType); + + // Display the scope + $html .= ' ' . $this->handler->displayScope(); + } + + return $html; + } + } + } + + return $html; + } + + /** + * Find the first data-suffix attribute match available in the node + * e.g. will return 'one' + * + * @param DOMElement $node The node to parse + * + * @return mixed + * + * @since 3.3 + */ + protected function getNodeSuffix(DOMElement $node) + { + foreach ($this->suffix as $suffix) + { + if ($node->hasAttribute("data-$suffix")) + { + return $suffix; + } + } + + return null; + } + + /** + * Parse the HTML and replace the data-* HTML5 attributes with Microdata semantics + * + * @param string $html The HTML to parse + * + * @return string + * + * @since 3.3 + */ + public function parse($html) + { + // Create a new DOMDocument + $doc = new DOMDocument; + $doc->loadHTML($html); + + // Create a new DOMXPath, to make XPath queries + $xpath = new DOMXPath($doc); + + // Create the query pattern + $query = array(); + + foreach ($this->suffix as $suffix) + { + array_push($query, "//*[@data-" . $suffix . "]"); + } + + // Search for the data-* HTML5 attributes + $nodeList = $xpath->query(implode('|', $query)); + + // Replace each match + foreach ($nodeList as $node) + { + // Retrieve the params used to setup the JMicrodata library + $suffix = $this->getNodeSuffix($node); + $attribute = $node->getAttribute("data-" . $suffix); + $params = $this->parseParams($attribute); + + // Generate the Microdata semantic + $semantic = $this->display($params); + + // Replace the data-* HTML5 attributes with Microdata semantics + $pattern = '/data-' . $suffix . "=." . $attribute . "./"; + $html = preg_replace($pattern, $semantic, $html, 1); + } + + return $html; + } +} diff --git a/plugins/system/microdata/index.html b/plugins/system/microdata/index.html new file mode 100644 index 0000000000000..3af63015d2219 --- /dev/null +++ b/plugins/system/microdata/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/system/microdata/microdata.php b/plugins/system/microdata/microdata.php new file mode 100644 index 0000000000000..b0f4c9f1ec845 --- /dev/null +++ b/plugins/system/microdata/microdata.php @@ -0,0 +1,59 @@ +isAdmin() + || $app->getDocument()->getType() !== 'html') + { + return true; + } + + // Prevent site execution when editing + if ($app->isSite() && $app->input->get('layout') == 'edit') + { + return true; + } + + // Retrieve the params + $suffix = explode(',', $this->params->get('suffix', 'sd')); + + // Get the body HTML + $output = $app->getBody(); + + // Make an instance of the parser + $parser = new JMicrodataParser($suffix); + + // Set the body HTML + $app->setBody($parser->parse($output)); + } +} diff --git a/plugins/system/microdata/microdata.xml b/plugins/system/microdata/microdata.xml new file mode 100644 index 0000000000000..7ca4ebb1d698e --- /dev/null +++ b/plugins/system/microdata/microdata.xml @@ -0,0 +1,32 @@ + + + plg_system_microdata + Joomla! Project + August 2014 + Copyright (C) 2005 - 2014 Open Source Matters. All rights reserved. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + 3.0.0 + PLG_SYSTEM_MICRODATA_XML_DESCRIPTION + + microdata.php + index.html + lib + + + language/en-GB/en-GB.plg_system_microdata.ini + language/en-GB/en-GB.plg_system_microdata.sys.ini + + + +
+ +
+
+
+
\ No newline at end of file diff --git a/tests/unit/suites/libraries/joomla/microdata/JMicrodataParserTest.php b/tests/unit/suites/libraries/joomla/microdata/JMicrodataParserTest.php new file mode 100644 index 0000000000000..aff17aa3754e2 --- /dev/null +++ b/tests/unit/suites/libraries/joomla/microdata/JMicrodataParserTest.php @@ -0,0 +1,407 @@ +handler = new JMicrodataParser; + } + + /** + * Test the suffix() function + * + * @return void + * + * @since 3.3 + */ + public function testSuffix() + { + /** + * The attribute name should not contain any uppercase letters, + * and must be at least one character long after the prefix "data-" + */ + $this->handler->suffix(''); + $this->assertNotContains('', $this->handler->getSuffix()); + + // Test string input, convert to lowercase if if necessary + $this->handler->suffix('lowercaseSuffix'); + $this->assertContains('lowercasesuffix', $this->handler->getSuffix()); + + // Test array input + $this->handler->suffix(array('su', 'ff')); + $this->assertContains('su', $this->handler->getSuffix()); + $this->assertContains('ff', $this->handler->getSuffix()); + } + + /** + * Test the removeSuffix() function + * + * @return void + * + * @since 3.3 + */ + public function testRemoveSuffix() + { + $this->handler->suffix('anything'); + $this->handler->removeSuffix('anything'); + $this->assertNotContains('anything', $this->handler->getSuffix()); + } + + /** + * Test the getSuffix() function + * + * @return void + * + * @since 3.3 + */ + public function testGetSuffix() + { + $this->assertInternalType('array', $this->handler->getSuffix()); + } + + /** + * Test the parseParam() function + * + * @return void + * + * @since 3.3 + */ + public function testParseParam() + { + // Setup + $obj = new JMicrodataParser; + $method = 'parseParam'; + + // Test a complete params string containing the expected Type + $this->assertEquals( + TestReflection::invoke($obj, $method, 'Type.property.EType'), + array( + 'type' => 'Type', + 'property' => 'property', + 'expectedType' => 'EType' + ) + ); + + // Test a complete params string + $this->assertEquals( + TestReflection::invoke($obj, $method, 'Type.property'), + array( + 'type' => 'Type', + 'property' => 'property', + 'expectedType' => null + ) + ); + + // Test a params string containing the property and the expected type + $this->assertEquals( + TestReflection::invoke($obj, $method, 'property.EType'), + array( + 'type' => null, + 'property' => 'property', + 'expectedType' => 'EType' + ) + ); + + // Test with only the Type param + $this->assertEquals( + TestReflection::invoke($obj, $method, ' Type'), + array( + 'type' => 'Type', + 'property' => null, + 'expectedType' => null + ) + ); + + // Test with only the property param + $this->assertEquals( + TestReflection::invoke($obj, $method, 'property'), + array( + 'type' => null, + 'property' => 'property', + 'expectedType' => null + ) + ); + + // Test a strange behaviour + $this->assertEquals( + TestReflection::invoke($obj, $method, '.Type.property'), + array( + 'type' => null, + 'property' => null, + 'expectedType' => null + ) + ); + + // Test an empty string + $this->assertEquals( + TestReflection::invoke($obj, $method, ' '), + array( + 'type' => null, + 'property' => null, + 'expectedType' => null + ) + ); + } + + /** + * Test the parseParams() function + * + * @return void + * + * @since 3.3 + */ + public function testParseParams() + { + // Setup + $obj = new JMicrodataParser; + $method = 'parseParams'; + + // Test a complete complex case (bad semantics practice, avoid in production!) + $this->assertEquals( + TestReflection::invoke($obj, $method, 'Type.property.EType sProperty.EType Type FType.fProperty gProperty'), + array( + 'setType' => 'Type', + 'fallbacks' => array( + 'specialized' => array( + 'Type' => array('property' => 'EType'), + 'FType' => array('fProperty' => null) + ), + 'global' => array( + 'sProperty' => 'EType', + 'gProperty' => null + ) + ) + ) + ); + + // Test with only the Type param + $this->assertEquals( + TestReflection::invoke($obj, $method, ' Type'), + array( + 'setType' => 'Type', + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array() + ) + ) + ); + + // Test with only the property param + $this->assertEquals( + TestReflection::invoke($obj, $method, 'property'), + array( + 'setType' => null, + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array('property' => null) + ) + ) + ); + + // Test with only the property and fallbacks params + $this->assertEquals( + TestReflection::invoke($obj, $method, 'property.EType FType.fProperty'), + array( + 'setType' => null, + 'fallbacks' => array( + 'specialized' => array( + 'FType' => array( + 'fProperty' => null + ) + ), + 'global' => array( + 'property' => 'EType' + ) + ) + ) + ); + + // Test with only the Type and fallbacksProperty params + $this->assertEquals( + TestReflection::invoke($obj, $method, 'Type fProperty'), + array( + 'setType' => 'Type', + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array( + 'fProperty' => null + ) + ) + ) + ); + + // Test a strange behaviour + $this->assertEquals( + TestReflection::invoke($obj, $method, ' .Type.property FType GType. fProperty'), + array( + 'setType' => 'FType', + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array( + 'fProperty' => null + ) + ) + ) + ); + + // Test an empty string + $this->assertEquals( + TestReflection::invoke($obj, $method, ' '), + array( + 'setType' => null, + 'fallbacks' => array( + 'specialized' => array(), + 'global' => array() + ) + ) + ); + } + + /** + * Test the parse() function + * + * @return void + * + * @since 3.3 + */ + public function testParse() + { + // Setup + $content = 'content'; + + // Test a complete complex case (bad semantics practice, avoid in production!) + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test it displays the scope and set the current Type, tag parse: data-*="Article" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test a 'specialized' fallback, tag parse: data-*="Article.author" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test a 'specialized' fallback with an expected Type, tag parse: data-*="Article.author.Person" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test a 'global' property, tag parse: data-*="author" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test a 'global' property with an expected Type, tag parse: data-*="author" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test a strange behaviour, should set the current Type and display the scope with the last match, tag parse: data-*="Article Person" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test it displays the 'specialized' fallback instead of the 'global' fallback, tag parse: data-*="Article.articleBody description" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test check the 'global' fallbacks order, tag parse: data-*="description articleBody" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test self-closing tag parse: data-*="datePublished" + $html = ""; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test tag parse: data-*="Article.propertyDoesNotExist" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test tag parse: data-*="TypeDoesNotExist.propertyDoesNotExist" + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + + // Test multiple suffix tag parse + $this->handler->suffix('custom'); + $html = "$content$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test multiple suffix tag parse, check that replaces only the first match + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "" + ); + + // Test that it doesn't parse an unregistered suffix + $html = "$content"; + $this->assertEquals( + $this->handler->parse($html), + "$content" + ); + } +}