├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── VERSION ├── bin ├── Console.php └── release.php ├── composer.json ├── phpunit.ci.xml ├── phpunit.dist.xml ├── src ├── Autolink.php ├── AutolinkStatic.php ├── HtmlBuilder.php └── LinkHelper.php └── test ├── AutolinkStaticTest.php └── AutolinkTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: UnitTest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | php-versions: [ '8.2', '8.3', '8.4' ] 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | # PHP 14 | - name: Setup PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: ${{ matrix.php-versions }} 18 | # extensions: mbstring 19 | - name: Get composer cache directory 20 | id: composercache 21 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 22 | - name: Cache composer dependencies 23 | uses: actions/cache@v2 24 | with: 25 | path: ${{ steps.composercache.outputs.dir }} 26 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 27 | restore-keys: ${{ runner.os }}-composer- 28 | - name: Install dependencies 29 | run: composer update --prefer-dist --prefer-stable --no-progress --no-suggest 30 | 31 | - name: Run test suite 32 | run: php vendor/bin/phpunit --configuration phpunit.ci.xml 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .* 3 | !.gitignore 4 | !.travis.yml 5 | !.github 6 | 7 | # Composer 8 | /vendor/* 9 | composer.lock 10 | 11 | # Test 12 | phpunit.xml 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4 8 | - 8.0 9 | - nightly 10 | 11 | matrix: 12 | allow_failures: 13 | - php: 8.0 14 | - php: nightly 15 | 16 | before_install: 17 | 18 | before_script: 19 | - composer install 20 | 21 | script: 22 | - ./vendor/bin/phpunit -v --configuration phpunit.travis.xml 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2019 Simon Asika 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Autolink Library 2 | 3 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/asika32764/php-autolink/ci.yml?style=for-the-badge) 4 | [![Packagist Version](https://img.shields.io/packagist/v/asika/autolink?style=for-the-badge) 5 | ](https://packagist.org/packages/asika/autolink) 6 | [![Packagist Downloads](https://img.shields.io/packagist/dt/asika/autolink?style=for-the-badge)](https://packagist.org/packages/asika/autolink) 7 | 8 | 9 | 10 | A library to auto convert URLs to links. 11 | 12 | ## Table of Content 13 | 14 | 15 | * [PHP Autolink Library](#php-autolink-library) 16 | * [Table of Content](#table-of-content) 17 | * [Requirement](#requirement) 18 | * [Installation via Composer](#installation-via-composer) 19 | * [Getting Started](#getting-started) 20 | * [Use Autolink Object](#use-autolink-object) 21 | * [Convert Text](#convert-text) 22 | * [Add Attributes](#add-attributes) 23 | * [Convert Email](#convert-email) 24 | * [Attributes Escaping](#attributes-escaping) 25 | * [Options](#options) 26 | * [`text_limit`](#text_limit) 27 | * [`auto_title`](#auto_title) 28 | * [`strip_scheme`](#strip_scheme) 29 | * [`escape`](#escape) 30 | * [`link_no_scheme`](#link_no_scheme) 31 | * [Scheme](#scheme) 32 | * [Link Builder](#link-builder) 33 | 34 | 35 | ## Requirement 36 | 37 | - Version 2.1.x require PHP 8.2 or higher. 38 | - Version 2.0.x require PHP 8.0 or higher. 39 | - Version 1.x supports PHP 5.3 to 7.4 40 | 41 | ## Installation via Composer 42 | 43 | Add this to composer.json require block. 44 | 45 | ``` json 46 | { 47 | "require": { 48 | "asika/autolink": "^2.0" 49 | } 50 | } 51 | ``` 52 | 53 | ## Getting Started 54 | 55 | This is a quick start to convert URL to link: 56 | 57 | ```php 58 | use Asika\Autolink\AutolinkStatic; 59 | 60 | $text = AutolinkStatic::convert($text); 61 | $text = AutolinkStatic::convertEmail($text); 62 | ``` 63 | 64 | ## Use Autolink Object 65 | 66 | Create the object: 67 | 68 | ```php 69 | use Asika\Autolink\Autolink; 70 | 71 | $autolink = new Autolink(); 72 | ``` 73 | 74 | Create with options. 75 | 76 | ```php 77 | $options = [ 78 | 'strip_scheme' => false, 79 | 'text_limit' => null, 80 | 'auto_title' => false, 81 | 'escape' => true, 82 | 'link_no_scheme' => false 83 | ]; 84 | 85 | $schemes = ['http', 'https', 'skype', 'itunes']; 86 | 87 | $autolink = new Autolink($options, $schemes); 88 | ``` 89 | 90 | ## Convert Text 91 | 92 | This is an example text: 93 | 94 | ``` html 95 | This is Simple URL: 96 | http://www.google.com.tw 97 | 98 | This is SSL URL: 99 | https://www.google.com.tw 100 | 101 | This is URL with multi-level query: 102 | http://example.com/?foo[1]=a&foo[2]=b 103 | ``` 104 | 105 | We convert all URLs. 106 | 107 | ```php 108 | $text = $autolink->convert($text); 109 | ``` 110 | 111 | Output: 112 | 113 | ``` html 114 | This is Simple URL: 115 | http://www.google.com.tw 116 | 117 | This is SSL URL: 118 | https://www.google.com.tw 119 | 120 | This is URL with multi-level query: 121 | http://example.com/?foo[1]=a&foo[2]=b 122 | ``` 123 | 124 | ### Add Attributes 125 | 126 | ```php 127 | $text = $autolink->convert($text, ['class' => 'center']); 128 | ``` 129 | 130 | All link will add this attributes: 131 | 132 | ```php 133 | This is Simple URL: 134 | http://www.google.com.tw 135 | 136 | This is SSL URL: 137 | https://www.google.com.tw 138 | ``` 139 | 140 | ## Convert Email 141 | 142 | Email url has no scheme, we use anoter method to convert them, and it will add `mailto:` at begin of `href`. 143 | 144 | ```php 145 | $text = $autolink->convertEmail($text); 146 | ``` 147 | 148 | Output 149 | 150 | ``` html 151 | foo@example.com 152 | 153 | ``` 154 | 155 | ## Attributes Escaping 156 | 157 | As `htmlspecialchars()` in PHP 8.1 or higher will escape single quote as default, 158 | Autolink will also escape single quote even in 8.0. Use this method to keep all escape 159 | behavior same at any PHP versions: 160 | 161 | ```php 162 | $autolink->escape('...'); 163 | ``` 164 | 165 | If you want to change the escape behavior, set your custom escape handler: 166 | 167 | ```php 168 | $autolink->setEscapeHandler(fn => ...); 169 | ``` 170 | 171 | ## Options 172 | 173 | ### `text_limit` 174 | 175 | We can set this option by constructor or setter: 176 | 177 | ```php 178 | $autolink->textLimit(50); 179 | 180 | $text = $autolink->convert($text); 181 | ``` 182 | 183 | The link text will be: 184 | 185 | ``` 186 | http://campus.asukademy.com/learning/job/84-fin... 187 | ``` 188 | 189 | Use Your own limit handler by set a callback: 190 | 191 | ```php 192 | $autolink->textLimit(function($url) { 193 | return substr($url, 0, 50) . '...'; 194 | }); 195 | ``` 196 | 197 | Or use `\Asika\Autolink\LinkHelper::shorten()` Pretty handler: 198 | 199 | ```php 200 | $autolink->textLimit(function($url) { 201 | return \Asika\Autolink\Autolink::shortenUrl($url, 15, 6); 202 | }); 203 | ``` 204 | 205 | Output: 206 | 207 | ``` text 208 | http://campus.asukademy.com/....../84-find-interns...... 209 | ``` 210 | 211 | ### `auto_title` 212 | 213 | Use AutoTitle to force add title on anchor element. 214 | 215 | ```php 216 | $autolink->autoTitle(true); 217 | 218 | $text = $autolink->convert($text); 219 | ``` 220 | 221 | Output: 222 | 223 | ``` html 224 | http://www.google.com.tw 225 | ``` 226 | 227 | ### `strip_scheme` 228 | 229 | Strip Scheme on link text: 230 | 231 | ```php 232 | $autolink->stripScheme(true); 233 | 234 | $text = $autolink->convert($text); 235 | ``` 236 | 237 | Output 238 | 239 | ``` html 240 | www.google.com.tw 241 | ``` 242 | 243 | ### `escape` 244 | 245 | Auto escape URL, default is `true`: 246 | 247 | ```php 248 | $autolink->autoEscape(false); 249 | 250 | $text = $autolink->convert($text); 251 | 252 | $autolink->autoEscape(true); 253 | 254 | $text = $autolink->convert($text); 255 | ``` 256 | 257 | Output 258 | 259 | ``` html 260 | http://www.google.com.tw?foo=bar&yoo=baz 261 | http://www.google.com.tw?foo=bar&yoo=baz 262 | ``` 263 | 264 | ### `link_no_scheme` 265 | 266 | Convert URL which no scheme. If you pass `TRUE` to this option, Autolink will use 267 | `http` as default scheme, you can also provide your own default scheme. 268 | 269 | ```php 270 | $autolink->linkNoScheme('https'); 271 | 272 | $text = $autolink->convert('www.google.com.tw'); 273 | ``` 274 | 275 | Output 276 | 277 | ``` html 278 | www.google.com.tw 279 | ``` 280 | 281 | ## Scheme 282 | 283 | You can add new scheme to convert URL begin with it, for example: `vnc://example.com` 284 | 285 | ```php 286 | $autolink->addScheme('skype', 'vnc'); 287 | ``` 288 | 289 | Default schemes is `http, https, ftp, ftps`. 290 | 291 | ## Link Builder 292 | 293 | If you don't want to use `` element as your link, you can set a callback to build link HTML. 294 | 295 | ```php 296 | $autolink->setLinkBuilder(function(string $url, array $attribs) { 297 | $attribs['src'] = htmlspecialchars($url); 298 | 299 | return \Asika\Autolink\HtmlBuilder::create('img', $attribs, null); 300 | }); 301 | ``` 302 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.1.0 2 | -------------------------------------------------------------------------------- /bin/Console.php: -------------------------------------------------------------------------------- 1 | array('n', 'no', 'false', 0, '0', true), 60 | 1 => array('y', 'yes', 'true', 1, '1', false, null) 61 | ); 62 | 63 | /** 64 | * CliInput constructor. 65 | * 66 | * @param array $argv 67 | */ 68 | public function __construct($argv = null) 69 | { 70 | $this->parseArgv($argv ?: $_SERVER['argv']); 71 | 72 | $this->init(); 73 | } 74 | 75 | /** 76 | * init 77 | * 78 | * @return void 79 | */ 80 | protected function init() 81 | { 82 | // Override if necessary 83 | } 84 | 85 | /** 86 | * execute 87 | * 88 | * @param \Closure|null $callback 89 | * 90 | * @return int 91 | */ 92 | public function execute(\Closure $callback = null) 93 | { 94 | try { 95 | if ($this->getOption($this->helpOptions)) { 96 | $this->out($this->getHelp()); 97 | 98 | return 0; 99 | } 100 | 101 | if ($callback) { 102 | if (PHP_VERSION_ID >= 50400) { 103 | $callback = $callback->bindTo($this); 104 | } 105 | 106 | $result = call_user_func($callback, $this); 107 | } else { 108 | $result = $this->doExecute(); 109 | } 110 | } catch (\Exception $e) { 111 | $result = $this->handleException($e); 112 | } catch (\Throwable $e) { 113 | $result = $this->handleException($e); 114 | } 115 | 116 | if ($result === true) { 117 | $result = 0; 118 | } elseif ($result === false) { 119 | $result = 255; 120 | } else { 121 | $result = (bool) $result; 122 | } 123 | 124 | return (int) $result; 125 | } 126 | 127 | /** 128 | * doExecute 129 | * 130 | * @return mixed 131 | */ 132 | protected function doExecute() 133 | { 134 | // Please override this method. 135 | return 0; 136 | } 137 | 138 | /** 139 | * delegate 140 | * 141 | * @param string $method 142 | * 143 | * @return mixed 144 | */ 145 | protected function delegate($method) 146 | { 147 | $args = func_get_args(); 148 | array_shift($args); 149 | 150 | if (!is_callable(array($this, $method))) { 151 | throw new \LogicException(sprintf('Method: %s not found', $method)); 152 | } 153 | 154 | return call_user_func_array(array($this, $method), $args); 155 | } 156 | 157 | /** 158 | * getHelp 159 | * 160 | * @return string 161 | */ 162 | protected function getHelp() 163 | { 164 | return trim($this->help); 165 | } 166 | 167 | /** 168 | * handleException 169 | * 170 | * @param \Exception|\Throwable $e 171 | * 172 | * @return int 173 | */ 174 | protected function handleException($e) 175 | { 176 | $v = $this->getOption('v'); 177 | 178 | if ($e instanceof CommandArgsException) { 179 | $this->err('[Warning] ' . $e->getMessage()) 180 | ->err() 181 | ->err($this->getHelp()); 182 | } else { 183 | $this->err('[Error] ' . $e->getMessage()); 184 | } 185 | 186 | if ($v) { 187 | $this->err('[Backtrace]:') 188 | ->err($e->getTraceAsString()); 189 | } 190 | 191 | $code = $e->getCode(); 192 | 193 | return $code === 0 ? 255 : $code; 194 | } 195 | 196 | /** 197 | * getArgument 198 | * 199 | * @param int $offset 200 | * @param mixed $default 201 | * 202 | * @return mixed|null 203 | */ 204 | public function getArgument($offset, $default = null) 205 | { 206 | if (!isset($this->args[$offset])) { 207 | return $default; 208 | } 209 | 210 | return $this->args[$offset]; 211 | } 212 | 213 | /** 214 | * setArgument 215 | * 216 | * @param int $offset 217 | * @param mixed $value 218 | * 219 | * @return static 220 | */ 221 | public function setArgument($offset, $value) 222 | { 223 | $this->args[$offset] = $value; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * getOption 230 | * 231 | * @param string|array $name 232 | * @param mixed $default 233 | * 234 | * @return mixed|null 235 | */ 236 | public function getOption($name, $default = null) 237 | { 238 | $name = (array) $name; 239 | 240 | foreach ($name as $n) { 241 | if (isset($this->options[$n])) { 242 | return $this->options[$n]; 243 | } 244 | } 245 | 246 | return $default; 247 | } 248 | 249 | /** 250 | * setOption 251 | * 252 | * @param string|array $name 253 | * @param mixed $value 254 | * 255 | * @return static 256 | */ 257 | public function setOption($name, $value) 258 | { 259 | $name = (array) $name; 260 | 261 | foreach ($name as $n) { 262 | $this->options[$n] = $value; 263 | } 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * out 270 | * 271 | * @param string $text 272 | * @param boolean $nl 273 | * 274 | * @return static 275 | */ 276 | public function out($text = null, $nl = true) 277 | { 278 | fwrite(STDOUT, $text . ($nl ? "\n" : '')); 279 | 280 | return $this; 281 | } 282 | 283 | /** 284 | * err 285 | * 286 | * @param string $text 287 | * @param boolean $nl 288 | * 289 | * @return static 290 | */ 291 | public function err($text = null, $nl = true) 292 | { 293 | fwrite(STDERR, $text . ($nl ? "\n" : '')); 294 | 295 | return $this; 296 | } 297 | 298 | /** 299 | * in 300 | * 301 | * @param string $ask 302 | * @param mixed $default 303 | * 304 | * @return string 305 | */ 306 | public function in($ask = '', $default = null, $bool = false) 307 | { 308 | $this->out($ask, false); 309 | 310 | $in = rtrim(fread(STDIN, 8192), "\n\r"); 311 | 312 | if ($bool) { 313 | $in = $in === '' ? $default : $in; 314 | 315 | return (bool) $this->mapBoolean($in); 316 | } 317 | 318 | return $in === '' ? (string) $default : $in; 319 | } 320 | 321 | /** 322 | * mapBoolean 323 | * 324 | * @param string $in 325 | * 326 | * @return bool 327 | */ 328 | public function mapBoolean($in) 329 | { 330 | $in = strtolower((string) $in); 331 | 332 | if (in_array($in, $this->booleanMapping[0], true)) { 333 | return false; 334 | } 335 | 336 | if (in_array($in, $this->booleanMapping[1], true)) { 337 | return true; 338 | } 339 | 340 | return null; 341 | } 342 | 343 | /** 344 | * exec 345 | * 346 | * @param string $command 347 | * 348 | * @return static 349 | */ 350 | protected function exec($command) 351 | { 352 | $this->out('>> ' . $command); 353 | 354 | system($command); 355 | 356 | return $this; 357 | } 358 | 359 | /** 360 | * parseArgv 361 | * 362 | * @param array $argv 363 | * 364 | * @return void 365 | */ 366 | protected function parseArgv($argv) 367 | { 368 | $this->executable = array_shift($argv); 369 | $key = null; 370 | 371 | $out = array(); 372 | 373 | for ($i = 0, $j = count($argv); $i < $j; $i++) { 374 | $arg = $argv[$i]; 375 | 376 | // --foo --bar=baz 377 | if (0 === strpos($arg, '--')) { 378 | $eqPos = strpos($arg, '='); 379 | 380 | // --foo 381 | if ($eqPos === false) { 382 | $key = substr($arg, 2); 383 | 384 | // --foo value 385 | if ($i + 1 < $j && $argv[$i + 1][0] !== '-') { 386 | $value = $argv[$i + 1]; 387 | $i++; 388 | } else { 389 | $value = isset($out[$key]) ? $out[$key] : true; 390 | } 391 | 392 | $out[$key] = $value; 393 | } else { 394 | // --bar=baz 395 | $key = substr($arg, 2, $eqPos - 2); 396 | $value = substr($arg, $eqPos + 1); 397 | $out[$key] = $value; 398 | } 399 | } elseif (0 === strpos($arg, '-')) { 400 | // -k=value -abc 401 | 402 | // -k=value 403 | if (isset($arg[2]) && $arg[2] === '=') { 404 | $key = $arg[1]; 405 | $value = substr($arg, 3); 406 | $out[$key] = $value; 407 | } else { 408 | // -abc 409 | $chars = str_split(substr($arg, 1)); 410 | 411 | foreach ($chars as $char) { 412 | $key = $char; 413 | $out[$key] = isset($out[$key]) ? $out[$key] + 1 : 1; 414 | } 415 | 416 | // -a a-value 417 | if (($i + 1 < $j) && ($argv[$i + 1][0] !== '-') && (count($chars) === 1)) { 418 | $out[$key] = $argv[$i + 1]; 419 | $i++; 420 | } 421 | } 422 | } else { 423 | // Plain-arg 424 | $this->args[] = $arg; 425 | } 426 | } 427 | 428 | $this->options = $out; 429 | } 430 | } 431 | 432 | class CommandArgsException extends \RuntimeException 433 | { 434 | } 435 | -------------------------------------------------------------------------------- /bin/release.php: -------------------------------------------------------------------------------- 1 | 24 | 25 | [Options] 26 | h | help Show help information 27 | v Show more debug information. 28 | --dry-run Dry run without git push or commit. 29 | HELP; 30 | 31 | /** 32 | * doExecute 33 | * 34 | * @return bool|mixed 35 | * 36 | * @since __DEPLOY_VERSION__ 37 | */ 38 | protected function doExecute() 39 | { 40 | $currentVersion = trim(file_get_contents(__DIR__ . '/../VERSION')); 41 | $targetVersion = $this->getArgument(0); 42 | 43 | if (!$targetVersion) { 44 | $targetVersion = static::versionPlus($currentVersion, 1); 45 | } 46 | 47 | $this->out('Release version: ' . $targetVersion); 48 | 49 | static::writeVersion($targetVersion); 50 | $this->replaceDocblockTags($targetVersion); 51 | 52 | $this->exec(sprintf('git commit -am "Release version: %s"', $targetVersion)); 53 | $this->exec(sprintf('git tag %s', $targetVersion)); 54 | 55 | $this->exec('git push'); 56 | $this->exec('git push --tags'); 57 | 58 | return true; 59 | } 60 | 61 | /** 62 | * writeVersion 63 | * 64 | * @param string $version 65 | * 66 | * @return bool|int 67 | * 68 | * @since __DEPLOY_VERSION__ 69 | */ 70 | protected static function writeVersion(string $version) 71 | { 72 | return file_put_contents(static::versionFile(), $version . "\n"); 73 | } 74 | 75 | /** 76 | * versionFile 77 | * 78 | * @return string 79 | * 80 | * @since __DEPLOY_VERSION__ 81 | */ 82 | protected static function versionFile(): string 83 | { 84 | return __DIR__ . '/../VERSION'; 85 | } 86 | 87 | /** 88 | * versionPlus 89 | * 90 | * @param string $version 91 | * @param int $offset 92 | * @param string $suffix 93 | * 94 | * @return string 95 | * 96 | * @since __DEPLOY_VERSION__ 97 | */ 98 | protected static function versionPlus(string $version, int $offset, string $suffix = ''): string 99 | { 100 | [$version] = explode('-', $version, 2); 101 | 102 | $numbers = explode('.', $version); 103 | 104 | if (!isset($numbers[2])) { 105 | $numbers[2] = 0; 106 | } 107 | 108 | $numbers[2] += $offset; 109 | 110 | if ($numbers[2] === 0) { 111 | unset($numbers[2]); 112 | } 113 | 114 | $version = implode('.', $numbers); 115 | 116 | if ($suffix) { 117 | $version .= '-' . $suffix; 118 | } 119 | 120 | return $version; 121 | } 122 | 123 | /** 124 | * replaceDocblockTags 125 | * 126 | * @param string $version 127 | * 128 | * @return void 129 | */ 130 | protected function replaceDocblockTags(string $version): void 131 | { 132 | $this->out('Replacing Docblock...'); 133 | 134 | $files = new RecursiveIteratorIterator( 135 | new \RecursiveDirectoryIterator( 136 | __DIR__ . '/../src', 137 | \FilesystemIterator::SKIP_DOTS 138 | ) 139 | ); 140 | 141 | /** @var \SplFileInfo $file */ 142 | foreach ($files as $file) { 143 | if ($file->isDir() || $file->getExtension() !== 'php') { 144 | continue; 145 | } 146 | 147 | $content = file_get_contents($file->getPathname()); 148 | 149 | $content = str_replace( 150 | ['{DEPLOY_VERSION}', '__DEPLOY_VERSION__', '__LICENSE__', '${ORGANIZATION}', '{ORGANIZATION}'], 151 | [$version, $version, 'MIT License', 'LYRASOFT', 'LYRASOFT'], 152 | $content 153 | ); 154 | 155 | file_put_contents($file->getPathname(), $content); 156 | } 157 | 158 | $this->exec('git checkout master'); 159 | $this->exec(sprintf('git commit -am "Prepare for %s release."', $version)); 160 | $this->exec('git push origin master'); 161 | } 162 | 163 | /** 164 | * exec 165 | * 166 | * @param string $command 167 | * 168 | * @return static 169 | */ 170 | protected function exec($command) 171 | { 172 | $this->out('>> ' . $command); 173 | 174 | if (!$this->getOption('dry-run')) { 175 | system($command); 176 | } 177 | 178 | return $this; 179 | } 180 | } 181 | 182 | exit((new Build())->execute()); 183 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asika/autolink", 3 | "description": "Auto convert url to link anchor.", 4 | "type": "library", 5 | "keywords": [ 6 | "link", 7 | "linkify", 8 | "autolink" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Simon Asika", 13 | "email": "asika32764@gmail.com" 14 | } 15 | ], 16 | "homepage": "https://github.com/asika32764/php-autolink", 17 | "minimum-stability": "beta", 18 | "require": { 19 | "php": ">=8.2" 20 | }, 21 | "require-dev": { 22 | "windwalker/test": "^4.0", 23 | "windwalker/utilities": "^4.0", 24 | "phpunit/phpunit": "^10.0||^11.0" 25 | }, 26 | "scripts": { 27 | "test": "phpunit" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Asika\\Autolink\\": "src/" 32 | } 33 | }, 34 | "config": { 35 | }, 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.ci.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | test 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /phpunit.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | test 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Autolink.php: -------------------------------------------------------------------------------- 1 | false, 21 | 'text_limit' => null, 22 | 'auto_title' => false, 23 | 'escape' => true, 24 | 'link_no_scheme' => false 25 | ]; 26 | 27 | /** 28 | * Property schemes. 29 | * 30 | * @var array 31 | */ 32 | protected array $schemes = [ 33 | 'http', 34 | 'https', 35 | 'ftp', 36 | 'ftps' 37 | ]; 38 | 39 | /** 40 | * Property defaultParsed. 41 | * 42 | * @var array 43 | */ 44 | protected static array $defaultParsed = [ 45 | 'scheme' => null, 46 | 'user' => null, 47 | 'pass' => null, 48 | 'host' => null, 49 | 'port' => null, 50 | 'path' => null, 51 | 'query' => null, 52 | 'fragment' => null 53 | ]; 54 | 55 | /** 56 | * Property linkBuilder. 57 | * 58 | * @var callable 59 | */ 60 | protected $linkBuilder; 61 | 62 | protected ?\Closure $escapeHandler = null; 63 | 64 | /** 65 | * Class init. 66 | * 67 | * @param array $options Basic options. 68 | * @param array $schemes 69 | */ 70 | public function __construct(array $options = [], array $schemes = []) 71 | { 72 | $this->options = array_merge($this->options, (array) $options); 73 | 74 | $this->setSchemes(...array_merge($this->schemes, $schemes)); 75 | } 76 | 77 | /** 78 | * render 79 | * 80 | * @param string $text 81 | * @param array $attribs 82 | * 83 | * @return string 84 | */ 85 | public function convert(string $text, array $attribs = []): string 86 | { 87 | $linkNoScheme = $this->getLinkNoScheme(); 88 | $staticDomains = '|localhost'; 89 | 90 | if ($linkNoScheme) { 91 | $schemeRegex = "[(%s)\:\/\/@]*"; 92 | $staticDomains = ''; 93 | } else { 94 | $schemeRegex = "(%s)\:\/\/"; 95 | } 96 | 97 | $schemeRegex = sprintf($schemeRegex, $this->getSchemes(true)); 98 | 99 | $regex = '/(([a-zA-Z]*=")*' . $schemeRegex . "([\-\p{L}\p{N}\p{M}]+\.[\p{L}\p{M}]{2,}$staticDomains)([\/\p{L}\p{N}\p{M}\-._~:?#\[\]@!$&'()*+,;=%\">]*)?)/u"; 100 | 101 | return preg_replace_callback( 102 | $regex, 103 | function ($matches) use ($attribs, $linkNoScheme) { 104 | $url = $matches[0]; 105 | 106 | preg_match('/[a-zA-Z]*\=\"(.*)/', $url, $inElements); 107 | 108 | if ($inElements) { 109 | return $url; 110 | } 111 | 112 | if ( 113 | $linkNoScheme 114 | && ( 115 | str_starts_with($url, '://') 116 | || str_starts_with($url, '@') 117 | ) 118 | ) { 119 | return $url; 120 | } 121 | 122 | $suffix = ''; 123 | 124 | if (str_ends_with($url, '.')) { 125 | $suffix = '.'; 126 | $url = substr($url, 0, -1); 127 | } 128 | 129 | return $this->link($url, $attribs) . $suffix; 130 | }, 131 | $text 132 | ); 133 | } 134 | 135 | /** 136 | * renderEmail 137 | * 138 | * @param string $text 139 | * @param array $attribs 140 | * 141 | * @return string 142 | */ 143 | public function convertEmail(string $text, array $attribs = []): string 144 | { 145 | $regex = "/(([a-zA-Z]*=\")*[a-zA-Z0-9!#$%&'*+-\/=?^_`{|}~:]+@[a-zA-Z0-9!#$%&'*+-\/=?^_`{|}~]+\.[a-zA-Z\">]{2,})/"; 146 | 147 | return preg_replace_callback( 148 | $regex, 149 | function ($matches) use ($attribs) { 150 | preg_match('/[a-zA-Z]*\=\"(.*)/', $matches[0], $inElements); 151 | 152 | if (!$inElements) { 153 | $email = $this->isAutoEscape() ? $this->escape($matches[0]) : $matches[0]; 154 | 155 | $attribs['href'] = 'mailto:' . $email; 156 | 157 | return $this->buildLink($matches[0], $attribs); 158 | } 159 | 160 | return $matches[0]; 161 | }, 162 | $text 163 | ); 164 | } 165 | 166 | /** 167 | * convert 168 | * 169 | * @param string $url 170 | * @param array $attribs 171 | * 172 | * @return string 173 | */ 174 | public function link(string $url, array $attribs = []): string 175 | { 176 | $content = $url; 177 | 178 | if ($this->isStripScheme()) { 179 | if (preg_match('!^(' . $this->getSchemes(true) . ')://!i', $content, $m)) { 180 | $content = substr($content, strlen($m[1]) + 3); 181 | } 182 | } 183 | 184 | if ($limit = $this->getTextLimit()) { 185 | if (is_callable($limit)) { 186 | $content = $limit($content); 187 | } else { 188 | $content = $this->shorten($content, $limit); 189 | } 190 | } 191 | 192 | $attribs['href'] = $this->isAutoEscape() ? $this->escape($url) : $url; 193 | 194 | if (($scheme = $this->getLinkNoScheme()) && !str_contains($attribs['href'], '://')) { 195 | $scheme = is_string($scheme) ? $scheme : 'http'; 196 | 197 | $attribs['href'] = $scheme . '://' . $attribs['href']; 198 | } 199 | 200 | if ($this->isAutoTitle()) { 201 | $attribs['title'] = $this->escape($url); 202 | } 203 | 204 | return $this->buildLink($content, $attribs); 205 | } 206 | 207 | /** 208 | * buildLink 209 | * 210 | * @param string|null $url 211 | * @param array $attribs 212 | * 213 | * @return string 214 | */ 215 | protected function buildLink(?string $url = null, array $attribs = []): string 216 | { 217 | if (is_callable($this->linkBuilder)) { 218 | return (string) ($this->linkBuilder)($url, $attribs); 219 | } 220 | 221 | return HtmlBuilder::create('a', $attribs, $this->escape($url)); 222 | } 223 | 224 | /** 225 | * autolinkLabel 226 | * 227 | * @param string $text 228 | * @param int $limit 229 | * 230 | * @return string 231 | */ 232 | public function shorten(string $text, int $limit): string 233 | { 234 | if (!$limit) { 235 | return $text; 236 | } 237 | 238 | if (strlen($text) > $limit) { 239 | return substr($text, 0, $limit - 3) . '...'; 240 | } 241 | 242 | return $text; 243 | } 244 | 245 | public function stripScheme(bool $value = false): static 246 | { 247 | return $this->setOption('strip_scheme', $value); 248 | } 249 | 250 | public function isStripScheme(): bool 251 | { 252 | return (bool) $this->getOption('strip_scheme'); 253 | } 254 | 255 | public function autoEscape(bool $value = true): static 256 | { 257 | return $this->setOption('escape', $value); 258 | } 259 | 260 | public function isAutoEscape(): bool 261 | { 262 | return (bool) $this->getOption('escape'); 263 | } 264 | 265 | /** 266 | * @param int|callable|null $value 267 | * 268 | * @return static 269 | */ 270 | public function textLimit(int|callable|null $value = null): static 271 | { 272 | return $this->setOption('text_limit', $value); 273 | } 274 | 275 | public function getTextLimit(): int|callable|null 276 | { 277 | $value = $this->getOption('text_limit'); 278 | 279 | // Fix for B/C 280 | if ($value === false) { 281 | $value = null; 282 | } 283 | 284 | return $value; 285 | } 286 | 287 | /** 288 | * autoTitle 289 | * 290 | * @param mixed $value 291 | * 292 | * @return static 293 | */ 294 | public function autoTitle(bool $value = false): static 295 | { 296 | return $this->setOption('auto_title', $value); 297 | } 298 | 299 | public function isAutoTitle(): bool 300 | { 301 | return (bool) $this->getOption('auto_title'); 302 | } 303 | 304 | /** 305 | * linkNoScheme 306 | * 307 | * @param bool $value 308 | * 309 | * @return static 310 | */ 311 | public function linkNoScheme(bool|string $value = false): static 312 | { 313 | return $this->setOption('link_no_scheme', $value); 314 | } 315 | 316 | public function getLinkNoScheme(): bool|string 317 | { 318 | return $this->getOption('link_no_scheme'); 319 | } 320 | 321 | /** 322 | * optionAccess 323 | * 324 | * @param string $name 325 | * @param mixed $value 326 | * 327 | * @return static 328 | */ 329 | protected function setOption(string $name, mixed $value = null): static 330 | { 331 | $this->options[$name] = $value; 332 | 333 | return $this; 334 | } 335 | 336 | protected function getOption(string $name, mixed $default = null): mixed 337 | { 338 | return $this->options[$name] ?? $default; 339 | } 340 | 341 | /** 342 | * addScheme 343 | * 344 | * @param string ...$schemes 345 | * 346 | * @return static 347 | */ 348 | public function addScheme(string ...$schemes): static 349 | { 350 | foreach ($schemes as $scheme) { 351 | $scheme = strtolower($scheme); 352 | $this->schemes[] = $scheme; 353 | } 354 | 355 | $this->schemes = array_unique($this->schemes); 356 | 357 | return $this; 358 | } 359 | 360 | /** 361 | * removeScheme 362 | * 363 | * @param string $scheme 364 | * 365 | * @return static 366 | */ 367 | public function removeScheme(string $scheme): static 368 | { 369 | $index = array_search($scheme, $this->schemes, true); 370 | 371 | if ($index !== false) { 372 | unset($this->schemes[$index]); 373 | } 374 | 375 | return $this; 376 | } 377 | 378 | /** 379 | * Method to get property Options 380 | * 381 | * @return array 382 | */ 383 | public function getOptions(): array 384 | { 385 | return $this->options; 386 | } 387 | 388 | /** 389 | * Method to set property options 390 | * 391 | * @param array $options 392 | * 393 | * @return static Return self to support chaining. 394 | */ 395 | public function setOptions(array $options): static 396 | { 397 | $this->options = $options; 398 | 399 | return $this; 400 | } 401 | 402 | /** 403 | * Method to get property Schemes 404 | * 405 | * @param bool $regex 406 | * 407 | * @return array|string 408 | */ 409 | public function getSchemes(bool $regex = false): array|string 410 | { 411 | if ($regex) { 412 | return implode('|', $this->schemes); 413 | } 414 | 415 | return $this->schemes; 416 | } 417 | 418 | /** 419 | * Method to set property schemes 420 | * 421 | * @param string ...$schemes 422 | * 423 | * @return static Return self to support chaining. 424 | */ 425 | public function setSchemes(string ...$schemes): static 426 | { 427 | $schemes = array_unique(array_map('strtolower', $schemes)); 428 | 429 | $this->schemes = $schemes; 430 | 431 | return $this; 432 | } 433 | 434 | /** 435 | * Method to get property LinkBuilder 436 | * 437 | * @return callable 438 | */ 439 | public function getLinkBuilder(): callable 440 | { 441 | return $this->linkBuilder; 442 | } 443 | 444 | /** 445 | * Method to set property linkBuilder 446 | * 447 | * @param callable $linkBuilder 448 | * 449 | * @return static Return self to support chaining. 450 | */ 451 | public function setLinkBuilder(callable $linkBuilder): static 452 | { 453 | if (!is_callable($linkBuilder)) { 454 | throw new \InvalidArgumentException('Please use a callable or Closure.'); 455 | } 456 | 457 | $this->linkBuilder = $linkBuilder; 458 | 459 | return $this; 460 | } 461 | 462 | /** 463 | * @param string $url 464 | * @param int $lastPartLimit 465 | * @param int $dots 466 | * 467 | * @return string 468 | */ 469 | public static function shortenUrl(string $url, int $lastPartLimit = 15, int $dots = 6): string 470 | { 471 | $parsed = array_merge(static::$defaultParsed, parse_url($url)); 472 | 473 | // @link http://php.net/manual/en/function.parse-url.php#106731 474 | $scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : ''; 475 | $host = $parsed['host'] ?? ''; 476 | $port = isset($parsed['port']) ? ':' . $parsed['port'] : ''; 477 | $user = $parsed['user'] ?? ''; 478 | $pass = isset($parsed['pass']) ? ':' . $parsed['pass'] : ''; 479 | $pass = ($user || $pass) ? "$pass@" : ''; 480 | $path = $parsed['path'] ?? ''; 481 | $query = isset($parsed['query']) ? '?' . $parsed['query'] : ''; 482 | $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; 483 | 484 | $first = $scheme . $user . $pass . $host . $port . '/'; 485 | 486 | $last = $path . $query . $fragment; 487 | 488 | if (!$last) { 489 | return $first; 490 | } 491 | 492 | if (strlen($last) <= $lastPartLimit) { 493 | return $first . $last; 494 | } 495 | 496 | $last = explode('/', $last); 497 | $last = array_pop($last); 498 | 499 | if (strlen($last) > $lastPartLimit) { 500 | $last = '/' . substr($last, 0, $lastPartLimit) . str_repeat('.', $dots); 501 | } 502 | 503 | return $first . str_repeat('.', $dots) . $last; 504 | } 505 | 506 | public function escape(string $str): string 507 | { 508 | return $this->getEscapeHandler()($str); 509 | } 510 | 511 | public function getEscapeHandler(): ?\Closure 512 | { 513 | return $this->escapeHandler 514 | // PHP 8.1 or higher will escape single quite 515 | ?? static fn ($str) => htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE); 516 | } 517 | 518 | public function setEscapeHandler(?\Closure $escapeHandler): static 519 | { 520 | $this->escapeHandler = $escapeHandler; 521 | 522 | return $this; 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /src/AutolinkStatic.php: -------------------------------------------------------------------------------- 1 | 'readonly', 40 | 'disabled' => 'disabled', 41 | 'multiple' => 'true', 42 | 'checked' => 'checked', 43 | 'selected' => 'selected', 44 | ]; 45 | 46 | /** 47 | * Create a html element. 48 | * 49 | * @param string $name Element tag name. 50 | * @param array $attribs Element attributes. 51 | * @param mixed $content Element content. 52 | * @param bool $forcePair Force pair it. 53 | * 54 | * @return string Created element string. 55 | */ 56 | public static function create( 57 | string $name, 58 | array $attribs = [], 59 | string $content = '', 60 | bool $forcePair = false 61 | ): string { 62 | $forcePair = $forcePair ?: !in_array(strtolower($name), static::$unpairedElements, true); 63 | 64 | $name = trim($name); 65 | 66 | $tag = '<' . $name; 67 | 68 | $tag .= static::buildAttributes($attribs); 69 | 70 | if ($content !== null) { 71 | $tag .= '>' . $content . ''; 72 | } else { 73 | $tag .= $forcePair ? '>' : ' />'; 74 | } 75 | 76 | return $tag; 77 | } 78 | 79 | /** 80 | * buildAttributes 81 | * 82 | * @param array $attribs 83 | * 84 | * @return string 85 | */ 86 | public static function buildAttributes(array $attribs): string 87 | { 88 | $attribs = static::mapAttrValues($attribs); 89 | 90 | $string = ''; 91 | 92 | foreach ((array) $attribs as $key => $value) { 93 | if ($value === true) { 94 | $string .= ' ' . $key; 95 | 96 | continue; 97 | } 98 | 99 | if ($value === null || $value === false) { 100 | continue; 101 | } 102 | 103 | $string .= ' ' . $key . '=' . static::quote($value); 104 | } 105 | 106 | return $string; 107 | } 108 | 109 | /** 110 | * quote 111 | * 112 | * @param string $value 113 | * 114 | * @return string 115 | */ 116 | public static function quote(string $value): string 117 | { 118 | return '"' . $value . '"'; 119 | } 120 | 121 | /** 122 | * mapAttrValues 123 | * 124 | * @param array $attribs 125 | * 126 | * @return array 127 | */ 128 | protected static function mapAttrValues(array $attribs): array 129 | { 130 | foreach (static::$trueValueMapping as $key => $value) { 131 | $attribs[$key] = !empty($attribs[$key]) ? $value : null; 132 | } 133 | 134 | return $attribs; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/LinkHelper.php: -------------------------------------------------------------------------------- 1 | null, 23 | 'user' => null, 24 | 'pass' => null, 25 | 'host' => null, 26 | 'port' => null, 27 | 'path' => null, 28 | 'query' => null, 29 | 'fragment' => null 30 | ]; 31 | 32 | /** 33 | * @param string $url 34 | * @param int $lastPartLimit 35 | * @param int $dots 36 | * 37 | * @return string 38 | * 39 | * @since 1.1.1 40 | * 41 | * @deprecated Use Autolink::shortenUrl() instead. 42 | */ 43 | public static function shorten(string $url, int $lastPartLimit = 15, int $dots = 6): string 44 | { 45 | $parsed = array_merge(static::$defaultParsed, parse_url($url)); 46 | 47 | // @link http://php.net/manual/en/function.parse-url.php#106731 48 | $scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : ''; 49 | $host = isset($parsed['host']) ? $parsed['host'] : ''; 50 | $port = isset($parsed['port']) ? ':' . $parsed['port'] : ''; 51 | $user = isset($parsed['user']) ? $parsed['user'] : ''; 52 | $pass = isset($parsed['pass']) ? ':' . $parsed['pass'] : ''; 53 | $pass = ($user || $pass) ? "$pass@" : ''; 54 | $path = isset($parsed['path']) ? $parsed['path'] : ''; 55 | $query = isset($parsed['query']) ? '?' . $parsed['query'] : ''; 56 | $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; 57 | 58 | $first = $scheme . $user . $pass . $host . $port . '/'; 59 | 60 | $last = $path . $query . $fragment; 61 | 62 | if (!$last) { 63 | return $first; 64 | } 65 | 66 | if (strlen($last) <= $lastPartLimit) { 67 | return $first . $last; 68 | } 69 | 70 | $last = explode('/', $last); 71 | $last = array_pop($last); 72 | 73 | if (strlen($last) > $lastPartLimit) { 74 | $last = '/' . substr($last, 0, $lastPartLimit) . str_repeat('.', $dots); 75 | } 76 | 77 | return $first . str_repeat('.', $dots) . $last; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/AutolinkStaticTest.php: -------------------------------------------------------------------------------- 1 | %s', $url, $url), AutolinkStatic::convert($url)); 35 | self::assertEquals(sprintf('%s', $url, $url), AutolinkStatic::link($url)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/AutolinkTest.php: -------------------------------------------------------------------------------- 1 | instance = new Autolink(); 35 | } 36 | 37 | /** 38 | * testConvert 39 | * 40 | * @return void 41 | */ 42 | public function testConvert() 43 | { 44 | $text = <<LINK 71 | http://example.com/path?foo[1]=a&foo[2]=b 72 | 73 |
74 |

http://example.com/path?foo[1]=a&foo[2]=b

75 | 76 | TEXT; 77 | 78 | $html = <<http://www.google.com.tw 81 | 82 | This is SSL URL: 83 | https://www.google.com.tw 84 | 85 | This is URL with path: 86 | http://www.google.com.tw/images 87 | 88 | This is URL with query: 89 | http://www.google.com.tw/search?q=foo&num=100 90 | 91 | This is URL with multi-level query: 92 | http://example.com/?foo[1]=a&foo[2]=b 93 | 94 | This is URL with fragment: 95 | http://example.com/path#top 96 | 97 | This is URL inline: http://example.com/path#top with test. 98 | 99 | This is an IDN URL: http://dømi.fo 100 | 101 | This is an IDN URL in Devanagari: http://सार्वभौमिक-स्वीकृति-परीक्षण.संगठन 102 | 103 | This is URL in HTML: 104 | LINK 105 | http://example.com/path?foo[1]=a&foo[2]=b 106 | 107 |
108 |

http://example.com/path?foo[1]=a&foo[2]=b

109 | 110 | HTML; 111 | 112 | self::assertStringSafeEquals($html, $this->instance->convert($text)); 113 | } 114 | 115 | /** 116 | * testConvert 117 | * 118 | * @return void 119 | */ 120 | public function testLink() 121 | { 122 | $url = 'http://www.google.com'; 123 | 124 | self::assertEquals( 125 | 'http://www.google.com', 126 | $this->instance->link($url, ['foo' => 'bar']) 127 | ); 128 | 129 | $this->instance->stripScheme(true); 130 | 131 | self::assertEquals( 132 | 'www.google.com', 133 | $this->instance->link($url, ['foo' => 'bar']) 134 | ); 135 | 136 | $this->instance->autoTitle(true); 137 | 138 | self::assertEquals( 139 | 'www.google.com', 140 | $this->instance->link($url, ['foo' => 'bar']) 141 | ); 142 | } 143 | 144 | /** 145 | * testTextLimit 146 | * 147 | * @return void 148 | */ 149 | public function testTextLimit() 150 | { 151 | $url = 'http://campus.asukademy.com/learning/job/84-find-internship-opportunity-through-platform.html'; 152 | 153 | $this->instance->textLimit(50); 154 | 155 | self::assertEquals( 156 | 'http://campus.asukademy.com/learning/job/84-fin...', 157 | $this->instance->link($url) 158 | ); 159 | 160 | $this->instance->textLimit(function ($url) { 161 | return Autolink::shortenUrl($url); 162 | }); 163 | 164 | self::assertEquals( 165 | 'http://campus.asukademy.com/....../84-find-interns......', 166 | $this->instance->link($url) 167 | ); 168 | } 169 | 170 | /** 171 | * testAutoTitle 172 | * 173 | * @return void 174 | */ 175 | public function testAutoTitle() 176 | { 177 | $url = 'http://example.com/path?foo["1"]=a&foo[\'2\']=b'; 178 | 179 | $this->instance->autoTitle(true); 180 | 181 | self::assertEquals( 182 | 'http://example.com/path?foo["1"]=a&foo['2']=b', 183 | $this->instance->link($url, ['foo' => 'bar']) 184 | ); 185 | } 186 | 187 | /** 188 | * testStripScheme 189 | * 190 | * @return void 191 | */ 192 | public function testStripScheme() 193 | { 194 | $this->instance->stripScheme(true); 195 | 196 | $url = 'http://campus.asukademy.com/learning/job/84-find-internship-opportunity-through-platform.html'; 197 | 198 | self::assertEquals( 199 | 'campus.asukademy.com/learning/job/84-find-internship-opportunity-through-platform.html', 200 | $this->instance->link($url) 201 | ); 202 | } 203 | 204 | public function testAddScheme() 205 | { 206 | $url = 'ftp://example.com'; 207 | 208 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 209 | 210 | $url = 'ftps://example.com'; 211 | 212 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 213 | 214 | $url = 'https://example.com'; 215 | 216 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 217 | 218 | $url = 'skype://example.com'; 219 | 220 | self::assertEquals($url, $this->instance->convert($url)); 221 | 222 | $this->instance->addScheme('skype'); 223 | 224 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 225 | } 226 | 227 | public function testLinkNoScheme() 228 | { 229 | $this->instance->linkNoScheme('http'); 230 | 231 | $url = 'ftp://example.com'; 232 | 233 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 234 | 235 | $url = 'example.com'; 236 | 237 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 238 | 239 | $url = 'https://example.com'; 240 | 241 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 242 | 243 | $url = 'skype://example.com'; 244 | 245 | self::assertEquals($url, $this->instance->convert($url)); 246 | 247 | $this->instance->addScheme('skype'); 248 | 249 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 250 | 251 | $url = 'dømi.fo'; 252 | 253 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 254 | 255 | $url = 'dømi.fo/dømi'; 256 | 257 | self::assertEquals('' . $url . '', $this->instance->convert($url)); 258 | } 259 | 260 | public function testLinkNoSchemeShouldIgnoreEmail(): void 261 | { 262 | $this->instance->linkNoScheme('http'); 263 | 264 | $url = 'ABC hello@email.com CBA'; 265 | 266 | self::assertEquals('ABC hello@email.com CBA', $this->instance->convert($url)); 267 | } 268 | 269 | /** 270 | * testGetAndSetScheme 271 | * 272 | * @return void 273 | */ 274 | public function testGetAndSetScheme() 275 | { 276 | $autolink = new Autolink([], ['a', 'b', 'http']); 277 | 278 | self::assertEquals(['http', 'https', 'ftp', 'ftps', 'a', 'b'], $autolink->getSchemes()); 279 | 280 | self::assertEquals('http|https|ftp|ftps|a|b', $autolink->getSchemes(true)); 281 | 282 | $autolink->setSchemes('skype'); 283 | 284 | self::assertEquals(['skype'], $autolink->getSchemes()); 285 | 286 | $autolink->setSchemes('mailto'); 287 | 288 | self::assertEquals(['mailto'], $autolink->getSchemes()); 289 | 290 | $autolink->setSchemes('mailto', 'mailto'); 291 | 292 | self::assertEquals(['mailto'], $autolink->getSchemes()); 293 | 294 | $autolink->removeScheme('mailto'); 295 | 296 | self::assertEquals([], $autolink->getSchemes()); 297 | } 298 | 299 | public function testAutoEscape() 300 | { 301 | $autolink = new Autolink(); 302 | 303 | $url = 'https://example.com/?foo=bar&yoo=baz'; 304 | 305 | self::assertEquals( 306 | '' . htmlspecialchars($url) . '', 307 | $autolink->convert($url) 308 | ); 309 | 310 | $autolink->autoEscape(false); 311 | 312 | self::assertEquals('' . htmlspecialchars($url) . '', $autolink->convert($url)); 313 | 314 | $url = 'hello+admin&test@example.org'; 315 | 316 | $autolink->autoEscape(true); 317 | 318 | self::assertEquals( 319 | '' . htmlspecialchars($url) . '', 320 | $autolink->convertEmail($url) 321 | ); 322 | 323 | $autolink->autoEscape(false); 324 | 325 | self::assertEquals( 326 | '' . htmlspecialchars($url) . '', 327 | $autolink->convertEmail($url) 328 | ); 329 | } 330 | 331 | public function testConvertEmail() 332 | { 333 | $text = <<LINK 341 | sakura@flower.com 342 |
343 |
344 | 345 | My email address is sakura@flower.com. 346 | 347 | and emails are coming in between(sakura@flower.com). 348 | TEXT; 349 | 350 | $html = <<sakura@flower.com 353 | 354 | This is Email inline: sakura@flower.com with test. 355 | 356 | This is Email in HTML: 357 | LINK 358 | sakura@flower.com 359 |
360 |
361 | 362 | My email address is sakura@flower.com. 363 | 364 | and emails are coming in between(sakura@flower.com). 365 | HTML; 366 | 367 | self::assertStringSafeEquals($html, $this->instance->convertEmail($text)); 368 | } 369 | 370 | /** 371 | * testSetLinkBuilder 372 | * 373 | * @return void 374 | */ 375 | public function testGetAndSetLinkBuilder() 376 | { 377 | $this->instance->setLinkBuilder(function ($url, $attribs) { 378 | return $url . json_encode($attribs); 379 | }); 380 | 381 | self::assertEquals( 382 | 'http://google.com{"foo":"bar","href":"http:\/\/google.com"}', 383 | $this->instance->link('http://google.com', ['foo' => 'bar']) 384 | ); 385 | 386 | self::assertInstanceOf('Closure', $this->instance->getLinkBuilder()); 387 | } 388 | 389 | public function testIgnoreTrailingDot(): void 390 | { 391 | $txt = 'Link to https://google.com.'; 392 | 393 | $html = $this->instance->convert($txt); 394 | 395 | assertEquals( 396 | 'Link to https://google.com.', 397 | $html, 398 | ); 399 | 400 | $txt = 'Link to https://google.com/search?foo=yoo.'; 401 | 402 | $html = $this->instance->convert($txt); 403 | 404 | assertEquals( 405 | 'Link to https://google.com/search?foo=yoo.', 406 | $html, 407 | ); 408 | } 409 | 410 | public function testLocalhost(): void 411 | { 412 | $txt = 'Link to http://localhost with some text.'; 413 | 414 | $html = $this->instance->convert($txt); 415 | 416 | assertEquals( 417 | 'Link to http://localhost with some text.', 418 | $html, 419 | ); 420 | 421 | $txt = 'Link to http://localhost.'; 422 | 423 | $html = $this->instance->convert($txt); 424 | 425 | assertEquals( 426 | 'Link to http://localhost.', 427 | $html, 428 | ); 429 | 430 | // Localhost without scheme should be ignored. 431 | $txt = 'Link to localhost.'; 432 | 433 | $this->instance->linkNoScheme(true); 434 | $html = $this->instance->convert($txt); 435 | 436 | assertEquals( 437 | 'Link to localhost.', 438 | $html, 439 | ); 440 | } 441 | 442 | /** 443 | * urlProvider 444 | * 445 | * @return array 446 | */ 447 | public static function urlProvider() 448 | { 449 | return [ 450 | [ 451 | 'http://www.projectup.net/blog/index.php?option=com_content&view=article&id=15726:-agile-&catid=8:pmp-pm&Itemid=18', 452 | 'http://www.projectup.net/....../index.php?optio......', 453 | 15, 454 | 6, 455 | ], 456 | [ 457 | 'http://campus.asukademy.com/learning/job/84-find-internship-opportunity-through-platform.html', 458 | 'http://campus.asukademy.com/....../84-find-interns......', 459 | 15, 460 | 6, 461 | ], 462 | [ 463 | 'http://user:pass@campus.asukademy.com:8888/learning/job/84-find-internship-opportunity-through-platform.html', 464 | 'http://user:pass@campus.asukademy.com:8888/....../84-find-interns......', 465 | 15, 466 | 6, 467 | ], 468 | [ 469 | 'http://campus.asukademy.com/learning/job/84-find-internship-opportunity-through-platform.html', 470 | 'http://campus.asukademy.com/.../84-fi...', 471 | 5, 472 | 3, 473 | ], 474 | ]; 475 | } 476 | 477 | /** 478 | * testShorten 479 | * 480 | * @param $url 481 | * @param $expect 482 | * @param $limit 483 | * @param $dots 484 | */ 485 | #[DataProvider('urlProvider')] 486 | public function testShortenUrl($url, $expect, $limit, $dots) 487 | { 488 | self::assertEquals($expect, Autolink::shortenUrl($url, $limit, $dots)); 489 | } 490 | } 491 | --------------------------------------------------------------------------------