Autolink.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. <?php
  2. /**
  3. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  4. * @author Nick Pope <nick@nickpope.me.uk>
  5. * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
  6. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
  7. */
  8. namespace App\Util\Lexer;
  9. use Illuminate\Support\Str;
  10. /**
  11. * Twitter Autolink Class.
  12. *
  13. * Parses tweets and generates HTML anchor tags around URLs, usernames,
  14. * username/list pairs and hashtags.
  15. *
  16. * Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
  17. * is based on code by {@link http://github.com/mzsanford Matt Sanford} and
  18. * heavily modified by {@link http://github.com/ngnpope Nick Pope}.
  19. *
  20. * @author Mike Cochrane <mikec@mikenz.geek.nz>
  21. * @author Nick Pope <nick@nickpope.me.uk>
  22. * @copyright Copyright © 2010, Mike Cochrane, Nick Pope
  23. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
  24. */
  25. class Autolink extends Regex
  26. {
  27. /**
  28. * CSS class for auto-linked URLs.
  29. *
  30. * @var string
  31. */
  32. protected $class_url = '';
  33. /**
  34. * CSS class for auto-linked username URLs.
  35. *
  36. * @var string
  37. */
  38. protected $class_user = 'u-url mention';
  39. /**
  40. * CSS class for auto-linked list URLs.
  41. *
  42. * @var string
  43. */
  44. protected $class_list = 'u-url list-slug';
  45. /**
  46. * CSS class for auto-linked hashtag URLs.
  47. *
  48. * @var string
  49. */
  50. protected $class_hash = 'u-url hashtag';
  51. /**
  52. * CSS class for auto-linked cashtag URLs.
  53. *
  54. * @var string
  55. */
  56. protected $class_cash = 'u-url cashtag';
  57. /**
  58. * URL base for username links (the username without the @ will be appended).
  59. *
  60. * @var string
  61. */
  62. protected $url_base_user = null;
  63. /**
  64. * URL base for list links (the username/list without the @ will be appended).
  65. *
  66. * @var string
  67. */
  68. protected $url_base_list = null;
  69. /**
  70. * URL base for hashtag links (the hashtag without the # will be appended).
  71. *
  72. * @var string
  73. */
  74. protected $url_base_hash = null;
  75. /**
  76. * URL base for cashtag links (the hashtag without the $ will be appended).
  77. *
  78. * @var string
  79. */
  80. protected $url_base_cash = null;
  81. /**
  82. * Whether to include the value 'nofollow' in the 'rel' attribute.
  83. *
  84. * @var bool
  85. */
  86. protected $nofollow = true;
  87. /**
  88. * Whether to include the value 'noopener' in the 'rel' attribute.
  89. *
  90. * @var bool
  91. */
  92. protected $noopener = true;
  93. /**
  94. * Whether to include the value 'external' in the 'rel' attribute.
  95. *
  96. * Often this is used to be matched on in JavaScript for dynamically adding
  97. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  98. * been undeprecated and thus the 'target' attribute can be used. If this is
  99. * set to false then the 'target' attribute will be output.
  100. *
  101. * @var bool
  102. */
  103. protected $external = true;
  104. /**
  105. * The scope to open the link in.
  106. *
  107. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  108. * since been reinstated in HTML 5. To output the 'target' attribute you
  109. * must disable the adding of the string 'external' to the 'rel' attribute.
  110. *
  111. * @var string
  112. */
  113. protected $target = '_blank';
  114. /**
  115. * attribute for invisible span tag.
  116. *
  117. * @var string
  118. */
  119. protected $invisibleTagAttrs = "style='position:absolute;left:-9999px;'";
  120. /**
  121. * @var Extractor
  122. */
  123. protected $extractor = null;
  124. /**
  125. * Provides fluent method chaining.
  126. *
  127. * @param string $tweet The tweet to be converted.
  128. * @param bool $full_encode Whether to encode all special characters.
  129. *
  130. * @see __construct()
  131. *
  132. * @return Autolink
  133. */
  134. public static function create($tweet = null, $full_encode = false)
  135. {
  136. return new static($tweet, $full_encode);
  137. }
  138. /**
  139. * Reads in a tweet to be parsed and converted to contain links.
  140. *
  141. * As the intent is to produce links and output the modified tweet to the
  142. * user, we take this opportunity to ensure that we escape user input.
  143. *
  144. * @see htmlspecialchars()
  145. *
  146. * @param string $tweet The tweet to be converted.
  147. * @param bool $escape Whether to escape the tweet (default: true).
  148. * @param bool $full_encode Whether to encode all special characters.
  149. */
  150. public function __construct($tweet = null, $escape = true, $full_encode = false)
  151. {
  152. if ($escape && !empty($tweet)) {
  153. if ($full_encode) {
  154. parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
  155. } else {
  156. parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
  157. }
  158. } else {
  159. parent::__construct($tweet);
  160. }
  161. $this->extractor = Extractor::create();
  162. $this->url_base_user = config('app.url').'/';
  163. $this->url_base_list = config('app.url').'/';
  164. $this->url_base_hash = config('app.url').'/discover/tags/';
  165. $this->url_base_cash = config('app.url').'/search?q=%24';
  166. }
  167. /**
  168. * CSS class for auto-linked URLs.
  169. *
  170. * @return string CSS class for URL links.
  171. */
  172. public function getURLClass()
  173. {
  174. return $this->class_url;
  175. }
  176. /**
  177. * CSS class for auto-linked URLs.
  178. *
  179. * @param string $v CSS class for URL links.
  180. *
  181. * @return Autolink Fluid method chaining.
  182. */
  183. public function setURLClass($v)
  184. {
  185. $this->class_url = trim($v);
  186. return $this;
  187. }
  188. /**
  189. * CSS class for auto-linked username URLs.
  190. *
  191. * @return string CSS class for username links.
  192. */
  193. public function getUsernameClass()
  194. {
  195. return $this->class_user;
  196. }
  197. /**
  198. * CSS class for auto-linked username URLs.
  199. *
  200. * @param string $v CSS class for username links.
  201. *
  202. * @return Autolink Fluid method chaining.
  203. */
  204. public function setUsernameClass($v)
  205. {
  206. $this->class_user = trim($v);
  207. return $this;
  208. }
  209. /**
  210. * CSS class for auto-linked username/list URLs.
  211. *
  212. * @return string CSS class for username/list links.
  213. */
  214. public function getListClass()
  215. {
  216. return $this->class_list;
  217. }
  218. /**
  219. * CSS class for auto-linked username/list URLs.
  220. *
  221. * @param string $v CSS class for username/list links.
  222. *
  223. * @return Autolink Fluid method chaining.
  224. */
  225. public function setListClass($v)
  226. {
  227. $this->class_list = trim($v);
  228. return $this;
  229. }
  230. /**
  231. * CSS class for auto-linked hashtag URLs.
  232. *
  233. * @return string CSS class for hashtag links.
  234. */
  235. public function getHashtagClass()
  236. {
  237. return $this->class_hash;
  238. }
  239. /**
  240. * CSS class for auto-linked hashtag URLs.
  241. *
  242. * @param string $v CSS class for hashtag links.
  243. *
  244. * @return Autolink Fluid method chaining.
  245. */
  246. public function setHashtagClass($v)
  247. {
  248. $this->class_hash = trim($v);
  249. return $this;
  250. }
  251. /**
  252. * CSS class for auto-linked cashtag URLs.
  253. *
  254. * @return string CSS class for cashtag links.
  255. */
  256. public function getCashtagClass()
  257. {
  258. return $this->class_cash;
  259. }
  260. /**
  261. * CSS class for auto-linked cashtag URLs.
  262. *
  263. * @param string $v CSS class for cashtag links.
  264. *
  265. * @return Autolink Fluid method chaining.
  266. */
  267. public function setCashtagClass($v)
  268. {
  269. $this->class_cash = trim($v);
  270. return $this;
  271. }
  272. /**
  273. * Whether to include the value 'nofollow' in the 'rel' attribute.
  274. *
  275. * @return bool Whether to add 'nofollow' to the 'rel' attribute.
  276. */
  277. public function getNoFollow()
  278. {
  279. return $this->nofollow;
  280. }
  281. /**
  282. * Whether to include the value 'nofollow' in the 'rel' attribute.
  283. *
  284. * @param bool $v The value to add to the 'target' attribute.
  285. *
  286. * @return Autolink Fluid method chaining.
  287. */
  288. public function setNoFollow($v)
  289. {
  290. $this->nofollow = $v;
  291. return $this;
  292. }
  293. /**
  294. * Whether to include the value 'external' in the 'rel' attribute.
  295. *
  296. * Often this is used to be matched on in JavaScript for dynamically adding
  297. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  298. * been undeprecated and thus the 'target' attribute can be used. If this is
  299. * set to false then the 'target' attribute will be output.
  300. *
  301. * @return bool Whether to add 'external' to the 'rel' attribute.
  302. */
  303. public function getExternal()
  304. {
  305. return $this->external;
  306. }
  307. /**
  308. * Whether to include the value 'external' in the 'rel' attribute.
  309. *
  310. * Often this is used to be matched on in JavaScript for dynamically adding
  311. * the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
  312. * been undeprecated and thus the 'target' attribute can be used. If this is
  313. * set to false then the 'target' attribute will be output.
  314. *
  315. * @param bool $v The value to add to the 'target' attribute.
  316. *
  317. * @return Autolink Fluid method chaining.
  318. */
  319. public function setExternal($v)
  320. {
  321. $this->external = $v;
  322. return $this;
  323. }
  324. /**
  325. * The scope to open the link in.
  326. *
  327. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  328. * since been reinstated in HTML 5. To output the 'target' attribute you
  329. * must disable the adding of the string 'external' to the 'rel' attribute.
  330. *
  331. * @return string The value to add to the 'target' attribute.
  332. */
  333. public function getTarget()
  334. {
  335. return $this->target;
  336. }
  337. /**
  338. * The scope to open the link in.
  339. *
  340. * Support for the 'target' attribute was deprecated in HTML 4.01 but has
  341. * since been reinstated in HTML 5. To output the 'target' attribute you
  342. * must disable the adding of the string 'external' to the 'rel' attribute.
  343. *
  344. * @param string $v The value to add to the 'target' attribute.
  345. *
  346. * @return Autolink Fluid method chaining.
  347. */
  348. public function setTarget($v)
  349. {
  350. $this->target = trim($v);
  351. return $this;
  352. }
  353. /**
  354. * Autolink with entities.
  355. *
  356. * @param string $tweet
  357. * @param array $entities
  358. *
  359. * @return string
  360. *
  361. * @since 1.1.0
  362. */
  363. public function autoLinkEntities($tweet = null, $entities = null)
  364. {
  365. if (is_null($tweet)) {
  366. $tweet = $this->tweet;
  367. }
  368. $text = '';
  369. $beginIndex = 0;
  370. foreach ($entities as $entity) {
  371. if (isset($entity['screen_name'])) {
  372. if(Str::startsWith($entity['screen_name'], '@')) {
  373. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  374. } else {
  375. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  376. }
  377. } else {
  378. $text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
  379. }
  380. if (isset($entity['url'])) {
  381. $text .= $this->linkToUrl($entity);
  382. } elseif (isset($entity['hashtag'])) {
  383. $text .= $this->linkToHashtag($entity, $tweet);
  384. } elseif (isset($entity['screen_name'])) {
  385. $text .= $this->linkToMentionAndList($entity);
  386. } elseif (isset($entity['cashtag'])) {
  387. $text .= $this->linkToCashtag($entity, $tweet);
  388. }
  389. $beginIndex = $entity['indices'][1];
  390. }
  391. $text .= StringUtils::substr($tweet, $beginIndex, StringUtils::strlen($tweet));
  392. return $text;
  393. }
  394. /**
  395. * Auto-link hashtags, URLs, usernames and lists, with JSON entities.
  396. *
  397. * @param string The tweet to be converted
  398. * @param mixed The entities info
  399. *
  400. * @return string that auto-link HTML added
  401. *
  402. * @since 1.1.0
  403. */
  404. public function autoLinkWithJson($tweet = null, $json = null)
  405. {
  406. // concatenate entities
  407. $entities = [];
  408. if (is_object($json)) {
  409. $json = $this->object2array($json);
  410. }
  411. if (is_array($json)) {
  412. foreach ($json as $key => $vals) {
  413. $entities = array_merge($entities, $json[$key]);
  414. }
  415. }
  416. // map JSON entity to twitter-text entity
  417. foreach ($entities as $idx => $entity) {
  418. if (!empty($entity['text'])) {
  419. $entities[$idx]['hashtag'] = $entity['text'];
  420. }
  421. }
  422. $entities = $this->extractor->removeOverlappingEntities($entities);
  423. return $this->autoLinkEntities($tweet, $entities);
  424. }
  425. /**
  426. * convert Object to Array.
  427. *
  428. * @param mixed $obj
  429. *
  430. * @return array
  431. */
  432. protected function object2array($obj)
  433. {
  434. $array = (array) $obj;
  435. foreach ($array as $key => $var) {
  436. if (is_object($var) || is_array($var)) {
  437. $array[$key] = $this->object2array($var);
  438. }
  439. }
  440. return $array;
  441. }
  442. /**
  443. * Auto-link hashtags, URLs, usernames and lists.
  444. *
  445. * @param string The tweet to be converted
  446. *
  447. * @return string that auto-link HTML added
  448. *
  449. * @since 1.1.0
  450. */
  451. public function autoLink($tweet = null)
  452. {
  453. if (is_null($tweet)) {
  454. $tweet = $this->tweet;
  455. }
  456. $entities = $this->extractor->extractURLWithoutProtocol(false)->extractEntitiesWithIndices($tweet);
  457. return $this->autoLinkEntities($tweet, $entities);
  458. }
  459. /**
  460. * Auto-link the @username and @username/list references in the provided text. Links to @username references will
  461. * have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
  462. * added.
  463. *
  464. * @return string that auto-link HTML added
  465. *
  466. * @since 1.1.0
  467. */
  468. public function autoLinkUsernamesAndLists($tweet = null)
  469. {
  470. if (is_null($tweet)) {
  471. $tweet = $this->tweet;
  472. }
  473. $entities = $this->extractor->extractMentionsOrListsWithIndices($tweet);
  474. return $this->autoLinkEntities($tweet, $entities);
  475. }
  476. /**
  477. * Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
  478. * added.
  479. *
  480. * @return string that auto-link HTML added
  481. *
  482. * @since 1.1.0
  483. */
  484. public function autoLinkHashtags($tweet = null)
  485. {
  486. if (is_null($tweet)) {
  487. $tweet = $this->tweet;
  488. }
  489. $entities = $this->extractor->extractHashtagsWithIndices($tweet);
  490. return $this->autoLinkEntities($tweet, $entities);
  491. }
  492. /**
  493. * Auto-link URLs in the Tweet text provided.
  494. * <p/>
  495. * This only auto-links URLs with protocol.
  496. *
  497. * @return string that auto-link HTML added
  498. *
  499. * @since 1.1.0
  500. */
  501. public function autoLinkURLs($tweet = null)
  502. {
  503. if (is_null($tweet)) {
  504. $tweet = $this->tweet;
  505. }
  506. $entities = $this->extractor->extractURLWithoutProtocol(false)->extractURLsWithIndices($tweet);
  507. return $this->autoLinkEntities($tweet, $entities);
  508. }
  509. /**
  510. * Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
  511. * added.
  512. *
  513. * @return string that auto-link HTML added
  514. *
  515. * @since 1.1.0
  516. */
  517. public function autoLinkCashtags($tweet = null)
  518. {
  519. if (is_null($tweet)) {
  520. $tweet = $this->tweet;
  521. }
  522. $entities = $this->extractor->extractCashtagsWithIndices($tweet);
  523. return $this->autoLinkEntities($tweet, $entities);
  524. }
  525. public function linkToUrl($entity)
  526. {
  527. if (!empty($this->class_url)) {
  528. $attributes['class'] = $this->class_url;
  529. }
  530. $attributes['href'] = $entity['url'];
  531. $linkText = $this->escapeHTML($entity['url']);
  532. if (!empty($entity['display_url']) && !empty($entity['expanded_url'])) {
  533. // Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
  534. // should contain the full original URL (expanded_url), not the display URL.
  535. //
  536. // Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
  537. // font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
  538. // Elements with font-size:0 get copied even though they are not visible.
  539. // Note that display:none doesn't work here. Elements with display:none don't get copied.
  540. //
  541. // Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
  542. // wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
  543. // everything with the tco-ellipsis class.
  544. //
  545. // As an example: The user tweets "hi http://longdomainname.com/foo"
  546. // This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
  547. // This will get rendered as:
  548. // <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
  549. // …
  550. // <!-- There's a chance the onCopy event handler might not fire. In case that happens,
  551. // we include an &nbsp; here so that the … doesn't bump up against the URL and ruin it.
  552. // The &nbsp; is inside the tco-ellipsis span so that when the onCopy handler *does*
  553. // fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
  554. // e.g. "hi http://longdomainname.com/foo".
  555. // <span style='font-size:0'>&nbsp;</span>
  556. // </span>
  557. // <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
  558. // http://longdomai
  559. // </span>
  560. // <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
  561. // nname.com/foo
  562. // </span>
  563. // <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
  564. // <span style='font-size:0'>&nbsp;</span>
  565. // …
  566. // </span>
  567. //
  568. // Exception: pic.socialhub.dev images, for which expandedUrl = "https://socialhub.dev/#!/username/status/1234/photo/1
  569. // For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
  570. // For a pic.socialhub.dev URL, the only elided part will be the "https://", so this is fine.
  571. $displayURL = $entity['display_url'];
  572. $expandedURL = $entity['expanded_url'];
  573. $displayURLSansEllipses = preg_replace('/…/u', '', $displayURL);
  574. $diplayURLIndexInExpandedURL = mb_strpos($expandedURL, $displayURLSansEllipses);
  575. if ($diplayURLIndexInExpandedURL !== false) {
  576. $beforeDisplayURL = mb_substr($expandedURL, 0, $diplayURLIndexInExpandedURL);
  577. $afterDisplayURL = mb_substr($expandedURL, $diplayURLIndexInExpandedURL + mb_strlen($displayURLSansEllipses));
  578. $precedingEllipsis = (preg_match('/\A…/u', $displayURL)) ? '…' : '';
  579. $followingEllipsis = (preg_match('/…\z/u', $displayURL)) ? '…' : '';
  580. $invisibleSpan = "<span {$this->invisibleTagAttrs}>";
  581. $linkText = "<span class='tco-ellipsis'>{$precedingEllipsis}{$invisibleSpan}&nbsp;</span></span>";
  582. $linkText .= "{$invisibleSpan}{$this->escapeHTML($beforeDisplayURL)}</span>";
  583. $linkText .= "<span class='js-display-url'>{$this->escapeHTML($displayURLSansEllipses)}</span>";
  584. $linkText .= "{$invisibleSpan}{$this->escapeHTML($afterDisplayURL)}</span>";
  585. $linkText .= "<span class='tco-ellipsis'>{$invisibleSpan}&nbsp;</span>{$followingEllipsis}</span>";
  586. } else {
  587. $linkText = $entity['display_url'];
  588. }
  589. $attributes['title'] = $entity['expanded_url'];
  590. } elseif (!empty($entity['display_url'])) {
  591. $linkText = $entity['display_url'];
  592. }
  593. return $this->linkToText($entity, $linkText, $attributes);
  594. }
  595. /**
  596. * @param array $entity
  597. * @param string $tweet
  598. *
  599. * @return string
  600. *
  601. * @since 1.1.0
  602. */
  603. public function linkToHashtag($entity, $tweet = null)
  604. {
  605. if (is_null($tweet)) {
  606. $tweet = $this->tweet;
  607. }
  608. $this->target = false;
  609. $attributes = [];
  610. $class = [];
  611. $hash = StringUtils::substr($tweet, $entity['indices'][0], 1);
  612. $linkText = $hash.$entity['hashtag'];
  613. $attributes['href'] = $this->url_base_hash.$entity['hashtag'].'?src=hash';
  614. $attributes['title'] = '#'.$entity['hashtag'];
  615. if (!empty($this->class_hash)) {
  616. $class[] = $this->class_hash;
  617. }
  618. if (preg_match(self::$patterns['rtl_chars'], $linkText)) {
  619. $class[] = 'rtl';
  620. }
  621. if (!empty($class)) {
  622. $attributes['class'] = implode(' ', $class);
  623. }
  624. return $this->linkToText($entity, $linkText, $attributes);
  625. }
  626. /**
  627. * @param array $entity
  628. *
  629. * @return string
  630. *
  631. * @since 1.1.0
  632. */
  633. public function linkToMentionAndList($entity)
  634. {
  635. $attributes = [];
  636. $screen_name = $entity['screen_name'];
  637. if (!empty($entity['list_slug'])) {
  638. // Replace the list and username
  639. $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
  640. $class = $this->class_list;
  641. $url = $this->url_base_list.$screen_name;
  642. } else {
  643. // Replace the username
  644. $linkText = Str::startsWith($screen_name, '@') ? $screen_name : '@'.$screen_name;
  645. $class = $this->class_user;
  646. $url = $this->url_base_user . $screen_name;
  647. }
  648. if (!empty($class)) {
  649. $attributes['class'] = $class;
  650. }
  651. $attributes['href'] = $url;
  652. return $this->linkToText($entity, $linkText, $attributes);
  653. }
  654. /**
  655. * @param array $entity
  656. * @param string $tweet
  657. *
  658. * @return string
  659. *
  660. * @since 1.1.0
  661. */
  662. public function linkToCashtag($entity, $tweet = null)
  663. {
  664. if (is_null($tweet)) {
  665. $tweet = $this->tweet;
  666. }
  667. $attributes = [];
  668. $doller = StringUtils::substr($tweet, $entity['indices'][0], 1);
  669. $linkText = $doller.$entity['cashtag'];
  670. $attributes['href'] = $this->url_base_cash.$entity['cashtag'];
  671. $attributes['title'] = $linkText;
  672. if (!empty($this->class_cash)) {
  673. $attributes['class'] = $this->class_cash;
  674. }
  675. return $this->linkToText($entity, $linkText, $attributes);
  676. }
  677. /**
  678. * @param array $entity
  679. * @param string $text
  680. * @param array $attributes
  681. *
  682. * @return string
  683. *
  684. * @since 1.1.0
  685. */
  686. public function linkToText(array $entity, $text, $attributes = [])
  687. {
  688. $rel = [];
  689. if ($this->external) {
  690. $rel[] = 'external';
  691. }
  692. if ($this->nofollow) {
  693. $rel[] = 'nofollow';
  694. }
  695. if ($this->noopener) {
  696. $rel[] = 'noopener';
  697. }
  698. if (!empty($rel)) {
  699. $attributes['rel'] = implode(' ', $rel);
  700. }
  701. if ($this->target) {
  702. $attributes['target'] = $this->target;
  703. }
  704. $link = '<a';
  705. foreach ($attributes as $key => $val) {
  706. $link .= ' '.$key.'="'.$this->escapeHTML($val).'"';
  707. }
  708. $link .= '>'.$text.'</a>';
  709. return $link;
  710. }
  711. /**
  712. * html escape.
  713. *
  714. * @param string $text
  715. *
  716. * @return string
  717. */
  718. protected function escapeHTML($text)
  719. {
  720. return htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
  721. }
  722. }