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 = '';
$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);
}