diff --git a/.travis.yml b/.travis.yml index 5d83ee2..b9d16ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ before_script: script: - if [ $DEFAULT -eq 1 ]; then vendor/bin/phpunit --exclude-group deprecated,tld --stderr; fi - if [ $TLD_TEST -eq 1 ]; then vendor/bin/phpunit --group tld --stderr; fi - - if [ $PHPCS -eq 1 ]; then vendor/bin/phpcs -psn --extensions=php --standard=PSR2 ./lib ./tests; fi + - if [ $PHPCS -eq 1 ]; then vendor/bin/phpcs -psn --extensions=php --standard=PSR12 ./lib ./tests; fi - if [ $CODECOVERAGE -eq 1 ]; then vendor/bin/phpunit --exclude-group deprecated,tld --stderr --coverage-clover=coverage.xml; fi after_success: diff --git a/build/build-emoji-regex.php b/build/build-emoji-regex.php index b94f7e2..67ce3c8 100644 --- a/build/build-emoji-regex.php +++ b/build/build-emoji-regex.php @@ -3,7 +3,7 @@ require dirname(__DIR__) . '/vendor/autoload.php'; $classFile = dirname(__DIR__) . '/lib/Twitter/Text/EmojiRegex.php'; -$emojiDataUrl = 'https://www.unicode.org/Public/emoji/11.0/emoji-test.txt'; +$emojiDataUrl = 'https://www.unicode.org/Public/emoji/12.1/emoji-test.txt'; // -- $emojiData = file($emojiDataUrl); diff --git a/composer.json b/composer.json index 7b0ced1..2129e9d 100644 --- a/composer.json +++ b/composer.json @@ -31,11 +31,11 @@ "type": "package", "package": { "name": "twitter/twitter-text", - "version": "3.0.0", + "version": "3.1.0", "source": { "url": "https://github.com/twitter/twitter-text.git", "type": "git", - "reference": "v3.0.0" + "reference": "v3.1.0" } } } diff --git a/lib/Twitter/Text/Autolink.php b/lib/Twitter/Text/Autolink.php index d03256d..f77d99a 100644 --- a/lib/Twitter/Text/Autolink.php +++ b/lib/Twitter/Text/Autolink.php @@ -93,23 +93,11 @@ class Autolink protected $url_base_cash = 'https://twitter.com/search?q=%24'; /** - * Whether to include the value 'nofollow' in the 'rel' attribute. - * - * @var bool - */ - protected $nofollow = true; - - /** - * Whether to include the value 'external' in the 'rel' attribute. + * the 'rel' attribute values. * - * Often this is used to be matched on in JavaScript for dynamically adding - * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has - * been undeprecated and thus the 'target' attribute can be used. If this is - * set to false then the 'target' attribute will be output. - * - * @var bool + * @var array */ - protected $external = true; + protected $rel = array('external', 'nofollow'); /** * The scope to open the link in. @@ -209,6 +197,24 @@ public function __construct($tweet = null, $escape = true, $full_encode = false) $this->extractor = Extractor::create(); } + /** + * Set CSS class to all link types. + * + * @param string $v CSS class for links. + * + * @return Autolink Fluid method chaining. + */ + public function setToAllLinkClasses($v) + { + $this->setURLClass($v); + $this->setUsernameClass($v); + $this->setListClass($v); + $this->setHashtagClass($v); + $this->setCashtagClass($v); + + return $this; + } + /** * CSS class for auto-linked URLs. * @@ -331,7 +337,7 @@ public function setCashtagClass($v) */ public function getNoFollow() { - return $this->nofollow; + return in_array('nofollow', $this->rel, true); } /** @@ -343,7 +349,15 @@ public function getNoFollow() */ public function setNoFollow($v) { - $this->nofollow = $v; + if ($v && !$this->getNoFollow()) { + $this->setRel('nofollow', true); + } + if (!$v && $this->getNoFollow()) { + $this->rel = array_filter($this->rel, function ($r) { + return $r !== 'nofollow'; + }); + } + return $this; } @@ -359,7 +373,7 @@ public function setNoFollow($v) */ public function getExternal() { - return $this->external; + return in_array('external', $this->rel, true); } /** @@ -376,7 +390,15 @@ public function getExternal() */ public function setExternal($v) { - $this->external = $v; + if ($v && !$this->getExternal()) { + $this->setRel('external', true); + } + if (!$v && $this->getExternal()) { + $this->rel = array_filter($this->rel, function ($r) { + return $r !== 'external'; + }); + } + return $this; } @@ -822,15 +844,9 @@ public function linkToCashtag($entity, $tweet = null) */ public function linkToText(array $entity, $text, $attributes = array()) { - $rel = array(); - if ($this->external) { - $rel[] = 'external'; - } - if ($this->nofollow) { - $rel[] = 'nofollow'; - } - if (!empty($rel)) { - $attributes['rel'] = implode(' ', $rel); + $rel = $this->getRel(); + if ($rel !== '') { + $attributes['rel'] = $rel; } if ($this->target) { $attributes['target'] = $this->target; @@ -872,6 +888,39 @@ protected function linkToTextWithSymbol(array $entity, $symbol, $linkText, array return $this->linkToText($entity, $linkText, $attributes); } + /** + * get rel attribute + * + * @return string + */ + public function getRel() + { + $rel = $this->rel; + $rel = array_unique($rel); + + return implode(' ', $rel); + } + + /** + * Set rel attribute. + * + * This method override setExternal/setNoFollow setting. + * + * @param string[]|string $rel the rel attribute + * @param bool $merge if true, merge rel attributes instead replace. + * @return $this + */ + public function setRel($rel, $merge = false) + { + if (is_string($rel)) { + $rel = explode(' ', $rel); + } + + $this->rel = $merge ? array_unique(array_merge($this->rel, $rel)) : $rel; + + return $this; + } + /** * html escape * diff --git a/lib/Twitter/Text/EmojiRegex.php b/lib/Twitter/Text/EmojiRegex.php index 05d777a..95ae4c4 100644 --- a/lib/Twitter/Text/EmojiRegex.php +++ b/lib/Twitter/Text/EmojiRegex.php @@ -1,7 +1,8 @@ extractURLWithoutProtocol - || preg_match(Regex::getInvalidUrlWithoutProtocolPrecedingCharsMatcher(), $before)) { + if ( + !$this->extractURLWithoutProtocol + || preg_match(Regex::getInvalidUrlWithoutProtocolPrecedingCharsMatcher(), $before) + ) { continue; } @@ -359,8 +361,10 @@ public function extractURLsWithIndices($tweet) if (preg_match(Regex::getValidAsciiDomainMatcher(), $domain, $asciiDomain)) { // check hostname length - if (isset($asciiDomain[1]) - && strlen(rtrim($asciiDomain[1], '.')) > static::MAX_ASCII_HOSTNAME_LENGTH) { + if ( + isset($asciiDomain[1]) + && strlen(rtrim($asciiDomain[1], '.')) > static::MAX_ASCII_HOSTNAME_LENGTH + ) { continue; } @@ -374,9 +378,11 @@ public function extractURLsWithIndices($tweet) $start_position + $ascii_end_position ), ); - if (!empty($path) + if ( + !empty($path) || preg_match(Regex::getValidSpecialShortDomainMatcher(), $asciiDomain[0]) - || !preg_match(Regex::getInvalidCharactersMatcher(), $asciiDomain[0])) { + || !preg_match(Regex::getInvalidCharactersMatcher(), $asciiDomain[0]) + ) { $urls[] = $last_url; } } diff --git a/lib/Twitter/Text/ParseResults.php b/lib/Twitter/Text/ParseResults.php index 35e9ffa..c24f985 100644 --- a/lib/Twitter/Text/ParseResults.php +++ b/lib/Twitter/Text/ParseResults.php @@ -119,17 +119,25 @@ public function __get($name) */ public function __set($name, $value) { - if ($name === 'displayRangeStart' - && $this->lte($value, $this->displayTextRange[1], $name, 'displayRangeEnd')) { + if ( + $name === 'displayRangeStart' + && $this->lte($value, $this->displayTextRange[1], $name, 'displayRangeEnd') + ) { $this->displayTextRange[0] = (int)$value; - } elseif ($name === 'displayRangeEnd' - && $this->gte($value, $this->displayTextRange[0], $name, 'displayRangeStart')) { + } elseif ( + $name === 'displayRangeEnd' + && $this->gte($value, $this->displayTextRange[0], $name, 'displayRangeStart') + ) { $this->displayTextRange[1] = (int)$value; - } elseif ($name === 'validRangeStart' - && $this->lte($value, $this->validTextRange[1], $name, 'validRangeEnd')) { + } elseif ( + $name === 'validRangeStart' + && $this->lte($value, $this->validTextRange[1], $name, 'validRangeEnd') + ) { $this->validTextRange[0] = (int)$value; - } elseif ($name === 'validRangeEnd' - && $this->gte($value, $this->validTextRange[0], $name, 'validRangeStart')) { + } elseif ( + $name === 'validRangeEnd' + && $this->gte($value, $this->validTextRange[0], $name, 'validRangeStart') + ) { $this->validTextRange[1] = (int)$value; } elseif ($name === 'valid') { $this->result[$name] = (bool)$value; diff --git a/lib/Twitter/Text/Parser.php b/lib/Twitter/Text/Parser.php index 893249b..34ead6c 100644 --- a/lib/Twitter/Text/Parser.php +++ b/lib/Twitter/Text/Parser.php @@ -58,7 +58,7 @@ public function __construct(Configuration $config = null) public function parseTweet($tweet) { if ($tweet === null || '' === $tweet) { - return new ParseResults; + return new ParseResults(); } $normalizedTweet = StringUtils::normalizeFromNFC($tweet); @@ -96,7 +96,7 @@ public function parseTweet($tweet) $emojiLength = StringUtils::strlen($emoji); $charCount = StringUtils::charCount($emoji); - $weightedCount += $this->getCharacterWeight(StringUtils::substr($emoji, 0, 1), $this->config); + $weightedCount += $this->config->defaultWeight; $offset += $emojiLength; $displayOffset += $charCount; if ($weightedCount <= $maxWeightedTweetLength) { diff --git a/lib/Twitter/Text/TldLists.php b/lib/Twitter/Text/TldLists.php index bb987a2..276bc17 100644 --- a/lib/Twitter/Text/TldLists.php +++ b/lib/Twitter/Text/TldLists.php @@ -1619,7 +1619,7 @@ final public static function getValidGTLD() } $gTLD = implode('|', static::$gTLDs); - $regex = '(?:(?:' . $gTLD . ')(?=[^0-9a-z@]|$))'; + $regex = '(?:(?:' . $gTLD . ')(?=[^0-9a-z@+-]|$))'; return $regex; } @@ -1639,7 +1639,7 @@ final public static function getValidCcTLD() } $ccTLD = implode('|', static::$ccTLDs); - $regex = '(?:(?:' . $ccTLD . ')(?=[^0-9a-z@]|$))'; + $regex = '(?:(?:' . $ccTLD . ')(?=[^0-9a-z@+-]|$))'; return $regex; } diff --git a/lib/Twitter/Text/Validator.php b/lib/Twitter/Text/Validator.php index 89f6746..50dea27 100644 --- a/lib/Twitter/Text/Validator.php +++ b/lib/Twitter/Text/Validator.php @@ -188,13 +188,15 @@ public function isValidURL($url, $unicode_domains = true, $require_protocol = tr list($scheme, $authority, $path, $query, $fragment) = array_pad($matches, 5, ''); # Check scheme, path, query, fragment: - if (($require_protocol && !( + if ( + ($require_protocol && !( self::isValidMatch($scheme, Regex::getValidateUrlSchemeMatcher()) && preg_match('/^https?$/i', $scheme) )) || !self::isValidMatch($path, Regex::getValidateUrlPathMatcher()) || !self::isValidMatch($query, Regex::getValidateUrlQueryMatcher(), true) - || !self::isValidMatch($fragment, Regex::getValidateUrlFragmentMatcher(), true)) { + || !self::isValidMatch($fragment, Regex::getValidateUrlFragmentMatcher(), true) + ) { return false; } diff --git a/tests/TestCase/AutolinkTest.php b/tests/TestCase/AutolinkTest.php index eef2de9..9ea9094 100644 --- a/tests/TestCase/AutolinkTest.php +++ b/tests/TestCase/AutolinkTest.php @@ -64,14 +64,14 @@ public function testAccessorMutator() $this->assertSame('_blank', $this->linker->getTarget()); $this->assertSame('', $this->linker->setTarget(false)->getTarget()); - $this->assertSame(true, $this->linker->getExternal()); - $this->assertSame(false, $this->linker->setExternal(false)->getExternal()); + $this->assertTrue($this->linker->getExternal()); + $this->assertFalse($this->linker->setExternal(false)->getExternal()); - $this->assertSame(true, $this->linker->getNoFollow()); - $this->assertSame(false, $this->linker->setNoFollow(false)->getNoFollow()); + $this->assertTrue($this->linker->getNoFollow()); + $this->assertFalse($this->linker->setNoFollow(false)->getNoFollow()); - $this->assertSame(false, $this->linker->isUsernameIncludeSymbol()); - $this->assertSame(true, $this->linker->setUsernameIncludeSymbol(true)->isUsernameIncludeSymbol()); + $this->assertFalse($this->linker->isUsernameIncludeSymbol()); + $this->assertTrue($this->linker->setUsernameIncludeSymbol(true)->isUsernameIncludeSymbol()); $this->assertSame('', $this->linker->getSymbolTag()); $this->assertSame('i', $this->linker->setSymbolTag('i')->getSymbolTag()); @@ -125,4 +125,95 @@ public function testSymbolTag() $expected = '@mention'; $this->assertSame($expected, $this->linker->autoLink($tweet)); } + + /** + * test for rel attribute + * + * @dataProvider dataWithRel + * @return void + */ + public function testWithRel($setupCallback, $expectedRel, $expectedAutolink) + { + $this->linker->setTarget(false); + $this->linker->setUsernameClass(''); + $linker = call_user_func($setupCallback, $this->linker); + + $this->assertSame($expectedRel, $linker->getRel()); + + $tweet = 'tweet @mention https://example.com'; + $this->assertSame($expectedAutolink, $linker->autoLink($tweet)); + } + + public function dataWithRel() + { + return array( + 'default' => array( + function (Autolink $linker) { + return $linker; + }, + 'external nofollow', + 'tweet @mention https://example.com', + ), + 'external=false, nofollow=false' => array( + function (Autolink $linker) { + return $linker->setExternal(false)->setNoFollow(false); + }, + '', + 'tweet @mention https://example.com', + ), + 'set rel as string' => array( + function (Autolink $linker) { + return $linker->setRel('noopener noreferrer'); + }, + 'noopener noreferrer', + 'tweet @mention https://example.com', + ), + 'set rel as array' => array( + function (Autolink $linker) { + return $linker->setRel(array('noopener', 'noreferrer')); + }, + 'noopener noreferrer', + 'tweet @mention https://example.com', + ), + 'set rel with merge' => array( + function (Autolink $linker) { + return $linker->setRel('noopener', true); + }, + 'external nofollow noopener', + 'tweet @mention https://example.com', + ), + ); + } + + /** + * setToAllLinkClasses can set class to all link types + */ + public function testSetToAllLinkClasses() + { + $this->assertSame('', $this->linker->getURLClass()); + $this->assertSame('tweet-url username', $this->linker->getUsernameClass()); + $this->assertSame('tweet-url list-slug', $this->linker->getListClass()); + $this->assertSame('tweet-url hashtag', $this->linker->getHashtagClass()); + $this->assertSame('tweet-url cashtag', $this->linker->getCashtagClass()); + + // set default css class + $this->assertSame($this->linker, $this->linker->setToAllLinkClasses('my-custom-class')); + $this->assertSame('my-custom-class', $this->linker->getURLClass(), 'getURLClass will return default class'); + $this->assertSame('my-custom-class', $this->linker->getUsernameClass(), 'getUsernameClass will return default class'); + $this->assertSame('my-custom-class', $this->linker->getListClass(), 'getListClass will return default class'); + $this->assertSame('my-custom-class', $this->linker->getHashtagClass(), 'getHashtagClass will return default class'); + $this->assertSame('my-custom-class', $this->linker->getCashtagClass(), 'getCashtagClass will return default class'); + + // override each classes + $this->linker->setURLClass('my-url-class'); + $this->linker->setUsernameClass('my-username-class'); + $this->linker->setListClass('my-list-class'); + $this->linker->setHashtagClass('my-hashtag-class'); + $this->linker->setCashtagClass('my-cashtag-class'); + $this->assertSame('my-url-class', $this->linker->getURLClass(), 'getURLClass will return specific class'); + $this->assertSame('my-username-class', $this->linker->getUsernameClass(), 'getUsernameClass will return specific class'); + $this->assertSame('my-list-class', $this->linker->getListClass(), 'getListClass will return specific class'); + $this->assertSame('my-hashtag-class', $this->linker->getHashtagClass(), 'getHashtagClass will return specific class'); + $this->assertSame('my-cashtag-class', $this->linker->getCashtagClass(), 'getCashtagClass will return specific class'); + } } diff --git a/tests/TestCase/ConfigurationTest.php b/tests/TestCase/ConfigurationTest.php index 44ab5a3..526be18 100644 --- a/tests/TestCase/ConfigurationTest.php +++ b/tests/TestCase/ConfigurationTest.php @@ -34,7 +34,7 @@ class ConfigurationTest extends TestCase */ protected function setUp() { - $this->config = new Configuration; + $this->config = new Configuration(); } /** diff --git a/tests/TestCase/EmojiRegexTest.php b/tests/TestCase/EmojiRegexTest.php index 169d8d4..e05ce0a 100644 --- a/tests/TestCase/EmojiRegexTest.php +++ b/tests/TestCase/EmojiRegexTest.php @@ -1,4 +1,5 @@ results = new ParseResults; + $this->results = new ParseResults(); } /** @@ -73,7 +73,7 @@ public function testConstruct() */ public function testConstructEmpty() { - $result = new ParseResults; + $result = new ParseResults(); $this->assertSame(0, $result->weightedLength); $this->assertSame(0, $result->permillage); diff --git a/tests/TestCase/ParserTest.php b/tests/TestCase/ParserTest.php index f4da00b..1ee168c 100644 --- a/tests/TestCase/ParserTest.php +++ b/tests/TestCase/ParserTest.php @@ -36,7 +36,7 @@ class ParserTest extends TestCase */ protected function setUp() { - $this->parser = new Parser; + $this->parser = new Parser(); } /** @@ -229,4 +229,22 @@ public function testParseTweetWith64CharDomainWithoutProtocol() $this->assertSame(0, $result->validRangeStart); $this->assertSame(67, $result->validRangeEnd); } + + /** + * test for parseTweet Count unicode emoji #, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + keycap (\x{20e3}) + */ + public function testParseTweetWithEmojiNumberWithKeycapWithoutVariantSelector() + { + $text = '1⃣'; + + $result = $this->parser->parseTweet($text); + + $this->assertSame(2, $result->weightedLength); + $this->assertTrue($result->valid); + $this->assertSame(7, $result->permillage); + $this->assertSame(0, $result->displayRangeStart); + $this->assertSame(1, $result->displayRangeEnd); + $this->assertSame(0, $result->validRangeStart); + $this->assertSame(1, $result->validRangeEnd); + } } diff --git a/tests/TestCase/StringUtilsTest.php b/tests/TestCase/StringUtilsTest.php index 4ccb7f0..c636a08 100644 --- a/tests/TestCase/StringUtilsTest.php +++ b/tests/TestCase/StringUtilsTest.php @@ -1,4 +1,5 @@ assertStringStartsWith('(?:(?:삼성|닷컴|', $regexp); - $this->assertStringEndsWith('|aaa|onion)(?=[^0-9a-z@]|$))', $regexp); + $this->assertStringEndsWith('|aaa|onion)(?=[^0-9a-z@+-]|$))', $regexp); $regexpCached = TldLists::getValidGTLD(); $this->assertSame($regexp, $regexpCached); @@ -31,7 +31,7 @@ public function testGetValidCcTLD() { $regexp = TldLists::getValidCcTLD(); $this->assertStringStartsWith('(?:(?:한국|香港|', $regexp); - $this->assertStringEndsWith('|ad|ac)(?=[^0-9a-z@]|$))', $regexp); + $this->assertStringEndsWith('|ad|ac)(?=[^0-9a-z@+-]|$))', $regexp); $regexpCached = TldLists::getValidCcTLD(); $this->assertSame($regexp, $regexpCached); diff --git a/tests/example.php b/tests/example.php index 0ba0d4b..0c50533 100644 --- a/tests/example.php +++ b/tests/example.php @@ -10,6 +10,7 @@ * @copyright Copyright © 2010, Mike Cochrane, Nick Pope * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0 */ + if (!defined('E_DEPRECATED')) { define('E_DEPRECATED', 8192); }