├── .gitignore ├── .phpcs.xml.dist ├── composer.json ├── composer.lock ├── phpstan.neon.dist ├── readme.md └── src ├── Exceptions └── InvalidHtmlException.php ├── Full.php ├── Quick.php └── functions.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sniffs for WordPress plugins 4 | 5 | 6 | src 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-atlantic/chophper", 3 | "description": "HTML text truncation library for PHP", 4 | "version": "1.0.1", 5 | "minimum-stability": "dev", 6 | "require": { 7 | "masterminds/html5": "^2.8.1" 8 | }, 9 | "require-dev": { 10 | "code-atlantic/coding-standards": "^1.1.0" 11 | }, 12 | "license": "MIT", 13 | "config": { 14 | "allow-plugins": { 15 | "dealerdirect/phpcodesniffer-composer-installer": true 16 | } 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Chophper\\": "src/" 21 | } 22 | }, 23 | "scripts": { 24 | "format": "vendor/bin/phpcbf --standard=.phpcs.xml.dist --report-summary --report-source", 25 | "lint": "vendor/bin/phpcs --standard=.phpcs.xml.dist" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "74ccbe36af039e7a02f69c2bd78ee76f", 8 | "packages": [ 9 | { 10 | "name": "masterminds/html5", 11 | "version": "2.8.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Masterminds/html5-php.git", 15 | "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf", 20 | "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-dom": "*", 25 | "php": ">=5.3.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8" 29 | }, 30 | "type": "library", 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "2.7-dev" 34 | } 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Masterminds\\": "src" 39 | } 40 | }, 41 | "notification-url": "https://packagist.org/downloads/", 42 | "license": [ 43 | "MIT" 44 | ], 45 | "authors": [ 46 | { 47 | "name": "Matt Butcher", 48 | "email": "technosophos@gmail.com" 49 | }, 50 | { 51 | "name": "Matt Farina", 52 | "email": "matt@mattfarina.com" 53 | }, 54 | { 55 | "name": "Asmir Mustafic", 56 | "email": "goetas@gmail.com" 57 | } 58 | ], 59 | "description": "An HTML5 parser and serializer.", 60 | "homepage": "http://masterminds.github.io/html5-php", 61 | "keywords": [ 62 | "HTML5", 63 | "dom", 64 | "html", 65 | "parser", 66 | "querypath", 67 | "serializer", 68 | "xml" 69 | ], 70 | "support": { 71 | "issues": "https://github.com/Masterminds/html5-php/issues", 72 | "source": "https://github.com/Masterminds/html5-php/tree/2.8.1" 73 | }, 74 | "time": "2023-05-10T11:58:31+00:00" 75 | } 76 | ], 77 | "packages-dev": [ 78 | { 79 | "name": "code-atlantic/coding-standards", 80 | "version": "1.1.0", 81 | "source": { 82 | "type": "git", 83 | "url": "https://github.com/code-atlantic/coding-standards.git", 84 | "reference": "953457fb0334f49ddfbf48ae4782bc4d91896ff7" 85 | }, 86 | "dist": { 87 | "type": "zip", 88 | "url": "https://api.github.com/repos/code-atlantic/coding-standards/zipball/953457fb0334f49ddfbf48ae4782bc4d91896ff7", 89 | "reference": "953457fb0334f49ddfbf48ae4782bc4d91896ff7", 90 | "shasum": "" 91 | }, 92 | "require": { 93 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", 94 | "phpcompatibility/phpcompatibility-wp": "^2.1.4", 95 | "wp-coding-standards/wpcs": "^3.0.0" 96 | }, 97 | "type": "phpcodesniffer-standard", 98 | "notification-url": "https://packagist.org/downloads/", 99 | "license": [ 100 | "MIT" 101 | ], 102 | "description": "Code Atlantic Coding Standards", 103 | "support": { 104 | "issues": "https://github.com/code-atlantic/coding-standards/issues", 105 | "source": "https://github.com/code-atlantic/coding-standards/tree/v1.1.0" 106 | }, 107 | "time": "2023-09-04T06:24:59+00:00" 108 | }, 109 | { 110 | "name": "dealerdirect/phpcodesniffer-composer-installer", 111 | "version": "v1.0.0", 112 | "source": { 113 | "type": "git", 114 | "url": "https://github.com/PHPCSStandards/composer-installer.git", 115 | "reference": "4be43904336affa5c2f70744a348312336afd0da" 116 | }, 117 | "dist": { 118 | "type": "zip", 119 | "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", 120 | "reference": "4be43904336affa5c2f70744a348312336afd0da", 121 | "shasum": "" 122 | }, 123 | "require": { 124 | "composer-plugin-api": "^1.0 || ^2.0", 125 | "php": ">=5.4", 126 | "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" 127 | }, 128 | "require-dev": { 129 | "composer/composer": "*", 130 | "ext-json": "*", 131 | "ext-zip": "*", 132 | "php-parallel-lint/php-parallel-lint": "^1.3.1", 133 | "phpcompatibility/php-compatibility": "^9.0", 134 | "yoast/phpunit-polyfills": "^1.0" 135 | }, 136 | "type": "composer-plugin", 137 | "extra": { 138 | "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" 139 | }, 140 | "autoload": { 141 | "psr-4": { 142 | "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" 143 | } 144 | }, 145 | "notification-url": "https://packagist.org/downloads/", 146 | "license": [ 147 | "MIT" 148 | ], 149 | "authors": [ 150 | { 151 | "name": "Franck Nijhof", 152 | "email": "franck.nijhof@dealerdirect.com", 153 | "homepage": "http://www.frenck.nl", 154 | "role": "Developer / IT Manager" 155 | }, 156 | { 157 | "name": "Contributors", 158 | "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" 159 | } 160 | ], 161 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin", 162 | "homepage": "http://www.dealerdirect.com", 163 | "keywords": [ 164 | "PHPCodeSniffer", 165 | "PHP_CodeSniffer", 166 | "code quality", 167 | "codesniffer", 168 | "composer", 169 | "installer", 170 | "phpcbf", 171 | "phpcs", 172 | "plugin", 173 | "qa", 174 | "quality", 175 | "standard", 176 | "standards", 177 | "style guide", 178 | "stylecheck", 179 | "tests" 180 | ], 181 | "support": { 182 | "issues": "https://github.com/PHPCSStandards/composer-installer/issues", 183 | "source": "https://github.com/PHPCSStandards/composer-installer" 184 | }, 185 | "time": "2023-01-05T11:28:13+00:00" 186 | }, 187 | { 188 | "name": "phpcompatibility/php-compatibility", 189 | "version": "9.3.5", 190 | "source": { 191 | "type": "git", 192 | "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", 193 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" 194 | }, 195 | "dist": { 196 | "type": "zip", 197 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", 198 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", 199 | "shasum": "" 200 | }, 201 | "require": { 202 | "php": ">=5.3", 203 | "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" 204 | }, 205 | "conflict": { 206 | "squizlabs/php_codesniffer": "2.6.2" 207 | }, 208 | "require-dev": { 209 | "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" 210 | }, 211 | "suggest": { 212 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", 213 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 214 | }, 215 | "type": "phpcodesniffer-standard", 216 | "notification-url": "https://packagist.org/downloads/", 217 | "license": [ 218 | "LGPL-3.0-or-later" 219 | ], 220 | "authors": [ 221 | { 222 | "name": "Wim Godden", 223 | "homepage": "https://github.com/wimg", 224 | "role": "lead" 225 | }, 226 | { 227 | "name": "Juliette Reinders Folmer", 228 | "homepage": "https://github.com/jrfnl", 229 | "role": "lead" 230 | }, 231 | { 232 | "name": "Contributors", 233 | "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" 234 | } 235 | ], 236 | "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", 237 | "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", 238 | "keywords": [ 239 | "compatibility", 240 | "phpcs", 241 | "standards" 242 | ], 243 | "support": { 244 | "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", 245 | "source": "https://github.com/PHPCompatibility/PHPCompatibility" 246 | }, 247 | "time": "2019-12-27T09:44:58+00:00" 248 | }, 249 | { 250 | "name": "phpcompatibility/phpcompatibility-paragonie", 251 | "version": "1.3.2", 252 | "source": { 253 | "type": "git", 254 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", 255 | "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26" 256 | }, 257 | "dist": { 258 | "type": "zip", 259 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", 260 | "reference": "bba5a9dfec7fcfbd679cfaf611d86b4d3759da26", 261 | "shasum": "" 262 | }, 263 | "require": { 264 | "phpcompatibility/php-compatibility": "^9.0" 265 | }, 266 | "require-dev": { 267 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7", 268 | "paragonie/random_compat": "dev-master", 269 | "paragonie/sodium_compat": "dev-master" 270 | }, 271 | "suggest": { 272 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 273 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 274 | }, 275 | "type": "phpcodesniffer-standard", 276 | "notification-url": "https://packagist.org/downloads/", 277 | "license": [ 278 | "LGPL-3.0-or-later" 279 | ], 280 | "authors": [ 281 | { 282 | "name": "Wim Godden", 283 | "role": "lead" 284 | }, 285 | { 286 | "name": "Juliette Reinders Folmer", 287 | "role": "lead" 288 | } 289 | ], 290 | "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", 291 | "homepage": "http://phpcompatibility.com/", 292 | "keywords": [ 293 | "compatibility", 294 | "paragonie", 295 | "phpcs", 296 | "polyfill", 297 | "standards", 298 | "static analysis" 299 | ], 300 | "support": { 301 | "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", 302 | "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" 303 | }, 304 | "time": "2022-10-25T01:46:02+00:00" 305 | }, 306 | { 307 | "name": "phpcompatibility/phpcompatibility-wp", 308 | "version": "2.1.4", 309 | "source": { 310 | "type": "git", 311 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", 312 | "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5" 313 | }, 314 | "dist": { 315 | "type": "zip", 316 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", 317 | "reference": "b6c1e3ee1c35de6c41a511d5eb9bd03e447480a5", 318 | "shasum": "" 319 | }, 320 | "require": { 321 | "phpcompatibility/php-compatibility": "^9.0", 322 | "phpcompatibility/phpcompatibility-paragonie": "^1.0" 323 | }, 324 | "require-dev": { 325 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7" 326 | }, 327 | "suggest": { 328 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 329 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 330 | }, 331 | "type": "phpcodesniffer-standard", 332 | "notification-url": "https://packagist.org/downloads/", 333 | "license": [ 334 | "LGPL-3.0-or-later" 335 | ], 336 | "authors": [ 337 | { 338 | "name": "Wim Godden", 339 | "role": "lead" 340 | }, 341 | { 342 | "name": "Juliette Reinders Folmer", 343 | "role": "lead" 344 | } 345 | ], 346 | "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", 347 | "homepage": "http://phpcompatibility.com/", 348 | "keywords": [ 349 | "compatibility", 350 | "phpcs", 351 | "standards", 352 | "static analysis", 353 | "wordpress" 354 | ], 355 | "support": { 356 | "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", 357 | "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" 358 | }, 359 | "time": "2022-10-24T09:00:36+00:00" 360 | }, 361 | { 362 | "name": "phpcsstandards/phpcsextra", 363 | "version": "dev-develop", 364 | "source": { 365 | "type": "git", 366 | "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", 367 | "reference": "7ef54bb093e935e7b530049b5696b6f74c33eaa8" 368 | }, 369 | "dist": { 370 | "type": "zip", 371 | "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/7ef54bb093e935e7b530049b5696b6f74c33eaa8", 372 | "reference": "7ef54bb093e935e7b530049b5696b6f74c33eaa8", 373 | "shasum": "" 374 | }, 375 | "require": { 376 | "php": ">=5.4", 377 | "phpcsstandards/phpcsutils": "^1.0.8", 378 | "squizlabs/php_codesniffer": "^3.7.1" 379 | }, 380 | "require-dev": { 381 | "php-parallel-lint/php-console-highlighter": "^1.0", 382 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 383 | "phpcsstandards/phpcsdevcs": "^1.1.6", 384 | "phpcsstandards/phpcsdevtools": "^1.2.1", 385 | "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" 386 | }, 387 | "default-branch": true, 388 | "type": "phpcodesniffer-standard", 389 | "extra": { 390 | "branch-alias": { 391 | "dev-stable": "1.x-dev", 392 | "dev-develop": "1.x-dev" 393 | } 394 | }, 395 | "notification-url": "https://packagist.org/downloads/", 396 | "license": [ 397 | "LGPL-3.0-or-later" 398 | ], 399 | "authors": [ 400 | { 401 | "name": "Juliette Reinders Folmer", 402 | "homepage": "https://github.com/jrfnl", 403 | "role": "lead" 404 | }, 405 | { 406 | "name": "Contributors", 407 | "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" 408 | } 409 | ], 410 | "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", 411 | "keywords": [ 412 | "PHP_CodeSniffer", 413 | "phpcbf", 414 | "phpcodesniffer-standard", 415 | "phpcs", 416 | "standards", 417 | "static analysis" 418 | ], 419 | "support": { 420 | "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", 421 | "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", 422 | "source": "https://github.com/PHPCSStandards/PHPCSExtra" 423 | }, 424 | "time": "2023-11-23T05:37:34+00:00" 425 | }, 426 | { 427 | "name": "phpcsstandards/phpcsutils", 428 | "version": "dev-develop", 429 | "source": { 430 | "type": "git", 431 | "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", 432 | "reference": "f76bab4ac55d559a22c24dafe1f35c0b940b5c02" 433 | }, 434 | "dist": { 435 | "type": "zip", 436 | "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f76bab4ac55d559a22c24dafe1f35c0b940b5c02", 437 | "reference": "f76bab4ac55d559a22c24dafe1f35c0b940b5c02", 438 | "shasum": "" 439 | }, 440 | "require": { 441 | "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", 442 | "php": ">=5.4", 443 | "squizlabs/php_codesniffer": "^3.7.1 || 4.0.x-dev@dev" 444 | }, 445 | "require-dev": { 446 | "ext-filter": "*", 447 | "php-parallel-lint/php-console-highlighter": "^1.0", 448 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 449 | "phpcsstandards/phpcsdevcs": "^1.1.6", 450 | "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" 451 | }, 452 | "default-branch": true, 453 | "type": "phpcodesniffer-standard", 454 | "extra": { 455 | "branch-alias": { 456 | "dev-stable": "1.x-dev", 457 | "dev-develop": "1.x-dev" 458 | } 459 | }, 460 | "autoload": { 461 | "classmap": [ 462 | "PHPCSUtils/" 463 | ] 464 | }, 465 | "notification-url": "https://packagist.org/downloads/", 466 | "license": [ 467 | "LGPL-3.0-or-later" 468 | ], 469 | "authors": [ 470 | { 471 | "name": "Juliette Reinders Folmer", 472 | "homepage": "https://github.com/jrfnl", 473 | "role": "lead" 474 | }, 475 | { 476 | "name": "Contributors", 477 | "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" 478 | } 479 | ], 480 | "description": "A suite of utility functions for use with PHP_CodeSniffer", 481 | "homepage": "https://phpcsutils.com/", 482 | "keywords": [ 483 | "PHP_CodeSniffer", 484 | "phpcbf", 485 | "phpcodesniffer-standard", 486 | "phpcs", 487 | "phpcs3", 488 | "standards", 489 | "static analysis", 490 | "tokens", 491 | "utility" 492 | ], 493 | "support": { 494 | "docs": "https://phpcsutils.com/", 495 | "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", 496 | "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", 497 | "source": "https://github.com/PHPCSStandards/PHPCSUtils" 498 | }, 499 | "time": "2023-11-24T21:05:41+00:00" 500 | }, 501 | { 502 | "name": "squizlabs/php_codesniffer", 503 | "version": "dev-master", 504 | "source": { 505 | "type": "git", 506 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 507 | "reference": "7763e2e1f773cb0615ed8afa133189fc804f583d" 508 | }, 509 | "dist": { 510 | "type": "zip", 511 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/7763e2e1f773cb0615ed8afa133189fc804f583d", 512 | "reference": "7763e2e1f773cb0615ed8afa133189fc804f583d", 513 | "shasum": "" 514 | }, 515 | "require": { 516 | "ext-simplexml": "*", 517 | "ext-tokenizer": "*", 518 | "ext-xmlwriter": "*", 519 | "php": ">=5.4.0" 520 | }, 521 | "require-dev": { 522 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 523 | }, 524 | "default-branch": true, 525 | "bin": [ 526 | "bin/phpcs", 527 | "bin/phpcbf" 528 | ], 529 | "type": "library", 530 | "extra": { 531 | "branch-alias": { 532 | "dev-master": "3.x-dev" 533 | } 534 | }, 535 | "notification-url": "https://packagist.org/downloads/", 536 | "license": [ 537 | "BSD-3-Clause" 538 | ], 539 | "authors": [ 540 | { 541 | "name": "Greg Sherwood", 542 | "role": "lead" 543 | } 544 | ], 545 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 546 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 547 | "keywords": [ 548 | "phpcs", 549 | "standards", 550 | "static analysis" 551 | ], 552 | "support": { 553 | "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", 554 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", 555 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" 556 | }, 557 | "time": "2023-11-02T00:47:31+00:00" 558 | }, 559 | { 560 | "name": "wp-coding-standards/wpcs", 561 | "version": "3.0.1", 562 | "source": { 563 | "type": "git", 564 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 565 | "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1" 566 | }, 567 | "dist": { 568 | "type": "zip", 569 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b4caf9689f1a0e4a4c632679a44e638c1c67aff1", 570 | "reference": "b4caf9689f1a0e4a4c632679a44e638c1c67aff1", 571 | "shasum": "" 572 | }, 573 | "require": { 574 | "ext-filter": "*", 575 | "ext-libxml": "*", 576 | "ext-tokenizer": "*", 577 | "ext-xmlreader": "*", 578 | "php": ">=5.4", 579 | "phpcsstandards/phpcsextra": "^1.1.0", 580 | "phpcsstandards/phpcsutils": "^1.0.8", 581 | "squizlabs/php_codesniffer": "^3.7.2" 582 | }, 583 | "require-dev": { 584 | "php-parallel-lint/php-console-highlighter": "^1.0.0", 585 | "php-parallel-lint/php-parallel-lint": "^1.3.2", 586 | "phpcompatibility/php-compatibility": "^9.0", 587 | "phpcsstandards/phpcsdevtools": "^1.2.0", 588 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 589 | }, 590 | "suggest": { 591 | "ext-iconv": "For improved results", 592 | "ext-mbstring": "For improved results" 593 | }, 594 | "type": "phpcodesniffer-standard", 595 | "notification-url": "https://packagist.org/downloads/", 596 | "license": [ 597 | "MIT" 598 | ], 599 | "authors": [ 600 | { 601 | "name": "Contributors", 602 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 603 | } 604 | ], 605 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 606 | "keywords": [ 607 | "phpcs", 608 | "standards", 609 | "static analysis", 610 | "wordpress" 611 | ], 612 | "support": { 613 | "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", 614 | "source": "https://github.com/WordPress/WordPress-Coding-Standards", 615 | "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" 616 | }, 617 | "funding": [ 618 | { 619 | "url": "https://opencollective.com/thewpcc/contribute/wp-php-63406", 620 | "type": "custom" 621 | } 622 | ], 623 | "time": "2023-09-14T07:06:09+00:00" 624 | } 625 | ], 626 | "aliases": [], 627 | "minimum-stability": "dev", 628 | "stability-flags": [], 629 | "prefer-stable": false, 630 | "prefer-lowest": false, 631 | "platform": [], 632 | "platform-dev": [], 633 | "plugin-api-version": "2.6.0" 634 | } 635 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src/ 5 | bootstrapFiles: 6 | 7 | scanDirectories: 8 | 9 | excludePaths: 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Chophper - A simple text truncation utility for HTML 2 | 3 | Chophper is a PHP utility for truncating text within HTML to a given length without breaking the HTML tags. 4 | 5 | Support for: 6 | 7 | - Truncate chars, optionally respecting word boundaries 8 | - Truncate words, optionally respecting sentence boundaries 9 | - Truncate sentences, optionally respecting block boundaries 10 | - Truncate blocks (paragraphs, lists, etc.) 11 | - Preserving HTML tags 12 | - Preserving HTML entities 13 | 14 | **Note: This is an alpha version. Use at your own risk, and expect API changes towards simple and more flexible usage before the first stable release.** 15 | 16 | ## Installation 17 | 18 | Install via composer: 19 | 20 | ```bash 21 | composer require code-atlantic/chophper 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```php 27 | // Full is built to fully support HTML5 without breaking the HTML structure. 28 | use Chophper\Full as Chophper; 29 | 30 | $options [ 31 | // ... see options below. 32 | ]; 33 | 34 | Chophper::truncate($html, $length, $options); 35 | ``` 36 | 37 | ## Options ( current, very subject to change ) 38 | 39 | | Option | Type | Default | Description | 40 | | --- | --- | --- | --- | 41 | | `ellipsis` | string | `…` | The string to append to the truncated text. | 42 | | `truncateBy` | string | `words` | Whether to break the text by chars, words, sentences or blocks | 43 | | `preserveWords` | boolean | `false` | Whether to preserve words when using chars truncation. | 44 | 45 | ## Options (wisthlist) 46 | 47 | | Option | Type | Default | Description | 48 | | --- | --- | --- | --- | 49 | | `wordBreak` | boolean | `false` | Whether to break the text at word boundaries. | 50 | | `preserveTags` | boolean | `false` | Whether to preserve HTML tags. | 51 | | `tagsWhitelist` | array | `[]` | A list of HTML tags to preserve. | 52 | | `tagsBlacklist` | array | `[]` | A list of HTML tags to remove. | 53 | | `tagsIngoreLength` | array | `[]` | A list of HTML tags to not count towards the length. | 54 | | `preserveEntities` | boolean | `false` | Whether to preserve HTML entities. | 55 | | `entitiesWhitelist` | array | `[]` | A list of HTML entities to preserve. | 56 | | `entitiesBlacklist` | array | `[]` | A list of HTML entities to remove. | 57 | | `preserveImages` | boolean | `false` | Whether to preserve images. | 58 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidHtmlException.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace Chophper\Exceptions; 11 | 12 | /** 13 | * Exception for invalid HTML. 14 | */ 15 | class InvalidHtmlException extends \Exception { 16 | } 17 | -------------------------------------------------------------------------------- /src/Full.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace Chophper; 11 | 12 | require_once __DIR__ . '/functions.php'; 13 | 14 | use DOMDocument; 15 | use Masterminds\HTML5; 16 | use Chophper\Exceptions\InvalidHtmlException; 17 | 18 | use function Chophper\ht_strlen; 19 | use function Chophper\ht_substr; 20 | use function Chophper\ht_strtolower; 21 | 22 | /** 23 | * Truncate HTML using full parser. 24 | */ 25 | class Full { 26 | 27 | /** 28 | * These tags can be truncated. 29 | * 30 | * @var array 31 | */ 32 | public static $ellipsable_tags = [ 33 | 'p', 34 | 'ol', 35 | 'ul', 36 | 'li', 37 | 'div', 38 | 'header', 39 | 'article', 40 | 'nav', 41 | 'section', 42 | 'footer', 43 | 'aside', 44 | 'dd', 45 | 'dt', 46 | 'dl', 47 | ]; 48 | 49 | /** 50 | * These tags are self-closing. 51 | * 52 | * @var array 53 | */ 54 | public static $self_closing_tags = [ 55 | 'br', 56 | 'hr', 57 | 'img', 58 | ]; 59 | 60 | /** 61 | * Parse options. 62 | * 63 | * @param string|array $opts Options. 64 | * 65 | * @return array 66 | */ 67 | public static function parse_options( $opts ) { 68 | $opts = array_merge( [ 69 | 'ellipsis' => '…', 70 | 'truncateBy' => 'words', // words, chars, sentences, blocks. 71 | 'preserveWords' => false, 72 | ], $opts ); 73 | 74 | return $opts; 75 | } 76 | 77 | /** 78 | * Get the root node of an HTML string. 79 | * 80 | * @param string $html HTML string. 81 | * 82 | * @return \DOMNode|null 83 | * 84 | * @throws InvalidHtmlException If the HTML is invalid. 85 | */ 86 | public static function get_root_node( $html ) { 87 | $root_node = null; 88 | 89 | // Parse using HTML5Lib if it's available. 90 | if ( class_exists( 'Masterminds\HTML5' ) ) { 91 | try { 92 | $html5 = new HTML5(); 93 | $doc = $html5->loadHTML( $html ); 94 | $root_node = $doc->documentElement->lastChild; 95 | } catch ( \Exception $e ) { 96 | // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 97 | error_log( 'HTML5Lib failed to parse HTML: ' . $e->getMessage() ); 98 | } 99 | } 100 | 101 | if ( null === $root_node ) { 102 | // HTML5Lib not available so we'll have to use DOMDocument 103 | // We'll only be able to parse HTML5 if it's valid XML. 104 | $doc = new DOMDocument(); 105 | $doc->formatOutput = false; 106 | $doc->preserveWhitespace = true; 107 | // loadHTML will fail with HTML5 tags (article, nav, etc) 108 | // so we need to suppress errors and if it fails to parse we 109 | // retry with the XML parser instead. 110 | $prev_use_errors = libxml_use_internal_errors( true ); 111 | if ( $doc->loadHTML( $html ) ) { 112 | $root_node = $doc->documentElement->lastChild->lastChild; 113 | } elseif ( $doc->loadXML( $html ) ) { 114 | $root_node = $doc->documentElement; 115 | } else { 116 | libxml_use_internal_errors( $prev_use_errors ); 117 | throw new InvalidHtmlException(); 118 | } 119 | libxml_use_internal_errors( $prev_use_errors ); 120 | } 121 | 122 | return $root_node; 123 | } 124 | 125 | /** 126 | * Truncate given HTML string to specified length. 127 | * If length_in_chars is false it's trimmed by number 128 | * of words, otherwise by number of characters. 129 | * 130 | * @param string $str string HTML string to truncate. 131 | * @param integer $length Length to truncate to. 132 | * @param string|array $opts Options. 133 | * 134 | * @return string 135 | * 136 | * @throws InvalidHtmlException If the HTML is invalid. 137 | */ 138 | public static function truncate( $str, $length, $opts = [] ) { 139 | $opts = static::parse_options( $opts ); 140 | 141 | $node = self::get_root_node( '
' . static::utf8_for_xml( $str ) . '
' ); 142 | 143 | if ( ! $node ) { 144 | return ''; 145 | } 146 | 147 | switch ( $opts['truncateBy'] ) { 148 | default: 149 | case 'words': 150 | case 'chars': 151 | case 'sentences': 152 | // Truncate by traversing the DOM tree, counting words as we go through each nodea recursively and truncating when we reach the desired length. 153 | $results = static::truncate_node( $node, $length, $opts ); 154 | break; 155 | 156 | case 'blocks': 157 | $results = static::truncateBy_blocks( $node, $length, $opts ); 158 | break; 159 | } 160 | 161 | list( $str ) = $results; 162 | 163 | // Strip off the root node div added before. 164 | $str = ht_substr( ht_substr( $str, 0, -6 ), 5 ); 165 | 166 | return $str; 167 | } 168 | 169 | /** 170 | * Truncate given HTML string to specified number of words. 171 | * 172 | * Truncate by traversing the DOM tree, counting words as we go through each 173 | * nodea recursively and truncating when we reach the desired length. 174 | * 175 | * @param \DOMNode $node Node to truncate. 176 | * @param integer $length Length to truncate to. 177 | * @param array $opts Options. 178 | * 179 | * @return array[string,integer,array} [0] Truncated inner contents. [1] Number of words remaining. [2] Options. 180 | */ 181 | protected static function truncate_node( $node, $length, $opts ) { 182 | $doc = $node->ownerDocument; 183 | 184 | // If the length is 0, return an empty string. 185 | if ( 0 === $length && ! static::is_ellipsable( $node ) ) { 186 | return [ 187 | '', 188 | 0, // TODO Why is this 1 and not 0? 189 | $opts, 190 | ]; 191 | } 192 | 193 | // Truncate the nodes inner contents recursively. 194 | list( $inner, $remaining, $opts ) = static::inner_truncate( $node, $length, $opts ); 195 | 196 | // If the inner contents are empty, return an empty string. 197 | if ( 0 === ht_strlen( $inner ) ) { 198 | return [ 199 | in_array( ht_strtolower( $node->nodeName ), static::$self_closing_tags, true ) 200 | // Self-closing tags should be returned as-is. 201 | ? $doc->saveXML( $node ) 202 | // Other tags should be returned as an empty string. 203 | : '', 204 | // Return the number of words remaining. 205 | // $length - $remaining, // TODO Review why is this not simply $remaining? 206 | $remaining < 0 ? 0 : $remaining, 207 | $opts, 208 | ]; 209 | } 210 | 211 | // Remove all child nodes from the node. 212 | while ( $node->firstChild ) { 213 | $node->removeChild( $node->firstChild ); 214 | } 215 | 216 | // Create a new document fragment to hold our truncated content. 217 | $new_node = $doc->createDocumentFragment(); 218 | 219 | // Append the inner contents to the fragment. 220 | $new_node->appendXml( $inner ); 221 | 222 | // Append the fragment to the node. 223 | $node->appendChild( $new_node ); 224 | 225 | // Return the truncated node. 226 | return [ 227 | $doc->saveXML( $node ), 228 | // $length - $remaining, // TODO Review why is this not simply $remaining? 229 | $remaining < 0 ? 0 : $remaining, 230 | $opts, 231 | ]; 232 | } 233 | 234 | /** 235 | * Truncate the inner contents of a node. 236 | * 237 | * @param \DOMNode $node Node to truncate. 238 | * @param integer $length Length to truncate to. 239 | * @param array $opts Options. 240 | * 241 | * @return array[string,integer,array} [0] Truncated inner contents. [1] Number of words remaining. [2] Options. 242 | */ 243 | protected static function inner_truncate( $node, $length, $opts ) { 244 | $inner = ''; 245 | $remaining = $length; 246 | 247 | foreach ( $node->childNodes as $child_node ) { 248 | if ( XML_ELEMENT_NODE === $child_node->nodeType ) { 249 | // Truncate nodes recursively. 250 | list( $text, $remaining, $opts ) = static::truncate_node( $child_node, $remaining, $opts ); 251 | } elseif ( XML_TEXT_NODE === $child_node->nodeType ) { 252 | // Process the child node, checking if it needs to be truncated, returning the truncated node and the number of words remaining. 253 | list( $text, $remaining, $opts ) = static::truncate_text( $child_node, $remaining, $opts ); 254 | } else { 255 | // If the node is not a text or element node, set the text to an empty string and the number of words to 0. 256 | $text = ''; 257 | } 258 | 259 | $inner .= $text; 260 | 261 | if ( $remaining <= 0 ) { 262 | if ( static::is_ellipsable( $node ) ) { 263 | $inner = preg_replace( '/(?:[\s\pP]+|(?:&(?:[a-z]+|#[0-9]+);?))*$/u', '', $inner ) . $opts['ellipsis']; 264 | $opts['ellipsis'] = ''; // TODO Find a better way to clear this, maybe based on $remaining. 265 | } 266 | break; 267 | } 268 | } 269 | 270 | return [ 271 | $inner, 272 | $remaining < 0 ? 0 : $remaining, 273 | $opts, 274 | ]; 275 | } 276 | 277 | /** 278 | * Truncate by root-level block elements like p, ul, ol, etc. 279 | * 280 | * @param \DOMNode $node Node to truncate. 281 | * @param integer $length Length to truncate to. 282 | * @param array $opts Options. 283 | * 284 | * @return array[string,integer,array} [0] Truncated inner contents. [1] Number of words remaining. [2] Options. 285 | */ 286 | protected static function truncateBy_blocks( $node, $length, $opts ) { 287 | $doc = $node->ownerDocument; 288 | 289 | $block_tags = [ 'p', 'ul', 'ol', 'div', 'header', 'article', 'nav', 'section', 'footer', 'aside', 'dd', 'dt', 'dl' ]; 290 | $remaining = $length; 291 | 292 | $nodes_to_keep = []; 293 | 294 | foreach ( $node->childNodes as $child_node ) { 295 | if ( $remaining <= 0 ) { 296 | break; 297 | } 298 | 299 | if ( XML_ELEMENT_NODE === $child_node->nodeType && in_array( ht_strtolower( $child_node->nodeName ), $block_tags, true ) ) { 300 | // If the node is a block element, add it to the fragment. 301 | $nodes_to_keep[] = $child_node; 302 | --$remaining; 303 | } elseif ( XML_TEXT_NODE === $child_node->nodeType && 0 !== ht_strlen( $child_node->textContent ) ) { 304 | // If the node is a non-empty text node, add it to the fragment. 305 | $nodes_to_keep[] = $child_node; 306 | } 307 | } 308 | 309 | $new_nodes = count( $nodes_to_keep ); 310 | 311 | // Remove all child nodes from the node. 312 | while ( $node->firstChild ) { 313 | $node->removeChild( $node->firstChild ); 314 | } 315 | 316 | // Loop over the nodes to process. 317 | foreach ( $nodes_to_keep as $i => $child_node ) { 318 | if ( 0 === $remaining && $i === $new_nodes - 1 ) { 319 | $child_node->appendChild( $doc->createTextNode( $opts['ellipsis'] ) ); 320 | $opts['ellipsis'] = ''; // TODO Find a better way to clear this, maybe based on $remaining. 321 | } 322 | 323 | // If the node is a block element, add it to the fragment. 324 | if ( XML_ELEMENT_NODE === $child_node->nodeType && in_array( ht_strtolower( $child_node->nodeName ), $block_tags, true ) ) { 325 | $node->appendChild( $child_node ); 326 | } elseif ( XML_TEXT_NODE === $child_node->nodeType ) { 327 | // If the node is a non-empty text node, add it to the fragment. 328 | $node->appendChild( $child_node ); 329 | } 330 | } 331 | 332 | // Return the truncated node. 333 | return [ 334 | $doc->saveXML( $node ), 335 | $remaining < 0 ? 0 : $remaining, 336 | $opts, 337 | ]; 338 | } 339 | 340 | /** 341 | * Truncate a text node. 342 | * 343 | * @param \DOMText $node Text node to truncate. 344 | * @param integer $length Length to truncate to. 345 | * @param array $opts Options. 346 | * 347 | * @return array[string,integer,array} [0] Truncated inner contents. [1] Number of words remaining. [2] Options. 348 | */ 349 | protected static function truncate_text( $node, $length, $opts ) { 350 | $doc = $node->ownerDocument; 351 | $xhtml = $doc->saveXML( $node ); 352 | 353 | switch ( $opts['truncateBy'] ) { 354 | default: 355 | case 'words': 356 | // Split the text into words. 357 | preg_match_all( '/\s*\S+/', $xhtml, $words ); 358 | 359 | // Get the words. 360 | $words = $words[0]; 361 | 362 | // Count the words and get the number of words remaining after truncation. 363 | $word_count = count( $words ); 364 | $remaining = $length - $word_count > 0 ? $length - $word_count : 0; 365 | 366 | // If the number of words is less than or equal to the length, return the text in full. 367 | if ( $length > $word_count ) { 368 | return [ 369 | // Return the full text. 370 | $xhtml, 371 | // Return the number of words remaining. 372 | $remaining, 373 | $opts, 374 | ]; 375 | } 376 | 377 | return [ 378 | // Slice the words array to the desired length and implode it back into a string. 379 | implode( '', array_slice( $words, 0, $length ) ), 380 | // Return the number of words remaining. 381 | $remaining, 382 | $opts, 383 | ]; 384 | 385 | case 'chars': 386 | // Split the text into words, excluding whitespace. 387 | preg_match_all( '/\s*\S+/', $xhtml, $words ); 388 | 389 | // Get the words. 390 | $words = $words[0]; 391 | 392 | // Count the words and get the number of words remaining after truncation. 393 | $char_count = ht_strlen( trim( $xhtml ) ); // Trim to prevent empty text nodes from counting as a character. 394 | 395 | // If the number of chars is less than or equal to the length, return the text in full. 396 | if ( $length > $char_count ) { 397 | return [ 398 | // Return the full text. 399 | $xhtml, 400 | // Return the number of chars remaining. 401 | $length - $char_count, 402 | $opts, 403 | ]; 404 | } 405 | 406 | if ( count( $words ) > 1 ) { 407 | $content = ''; 408 | 409 | $remaining = $length; 410 | 411 | // Loop through the words and add them to the content until we reach the desired length. 412 | foreach ( $words as $word ) { 413 | if ( $remaining <= 0 ) { 414 | break; 415 | } 416 | 417 | // If the length of the content plus the length of the word is greater than the desired length, break the loop. 418 | if ( ht_strlen( $content ) + ht_strlen( $word ) > $length ) { 419 | // If option for keeping words whole is set, break on the last word. 420 | if ( $opts['preserveWords'] ) { 421 | break; 422 | } else { 423 | // Trim the word to the remaining length. 424 | $word = ht_substr( $word, 0, $remaining ); 425 | } 426 | } 427 | 428 | // Add the word to the content. 429 | $content .= $word; 430 | $remaining -= ht_strlen( $word ); 431 | } 432 | 433 | return [ 434 | // Return the truncated content. 435 | $content, 436 | // Return the number of words remaining. 437 | $remaining < 0 ? 0 : $remaining, 438 | $opts, 439 | ]; 440 | } else { 441 | // If there is only one word, truncate it to the desired length. 442 | return [ 443 | // Truncate the text to the desired length. 444 | ht_substr( $node->textContent, 0, $length ), 445 | // Return the number of words remaining. 446 | $length - $char_count, 447 | $opts, 448 | ]; 449 | } 450 | break; 451 | 452 | case 'sentences': 453 | // Split the text into sentences. 454 | preg_match_all( '/(.*?[.!?]+)(?:\s|$)/us', $xhtml, $sentences ); 455 | 456 | // Get the sentences. 457 | $sentences = $sentences[0]; 458 | 459 | // Count the sentences. 460 | $sentence_count = count( $sentences ); 461 | $remaining = $length - $sentence_count; 462 | 463 | // If the number of sentences is less than or equal to the length, return the text in full. 464 | if ( $length >= $sentence_count ) { 465 | $remaining = $length - $sentence_count; 466 | 467 | return [ 468 | // Return the full text. 469 | $xhtml, 470 | // Return the number of sentences remaining. 471 | $remaining < 0 ? 0 : $remaining, 472 | $opts, 473 | ]; 474 | } 475 | 476 | return [ 477 | // Implode the sentences array back into a string. 478 | implode( '', array_slice( $sentences, 0, $length ) ), 479 | // Return the number of sentences remaining. 480 | 0, 481 | $opts, 482 | ]; 483 | } 484 | } 485 | 486 | /** 487 | * Check if a node is ellipsable. 488 | * 489 | * @param \DOMNode|\DOMDocument $node Node to truncate. 490 | * 491 | * @return boolean 492 | */ 493 | protected static function is_ellipsable( $node ) { 494 | return ( $node instanceof DOMDocument ) 495 | || in_array( ht_strtolower( $node->nodeName ), static::$ellipsable_tags, true ); 496 | } 497 | 498 | /** 499 | * Convert a string to UTF-8 for XML. 500 | * 501 | * @param string $str String to convert. 502 | * 503 | * @return string 504 | */ 505 | protected static function utf8_for_xml( $str ) { 506 | return preg_replace( '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $str ); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/Quick.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace Chophper; 11 | 12 | use Masterminds\HTML5; 13 | 14 | /** 15 | * Class to handle HTML truncation. 16 | */ 17 | class Quick { 18 | /** 19 | * HTML parser. 20 | * 21 | * @var HTML5 22 | */ 23 | public $parser; 24 | 25 | /** 26 | * Constructor. 27 | */ 28 | public function __construct() { 29 | $this->parser = new HTML5(); 30 | } 31 | 32 | /** 33 | * Strip HTML tags from a string. 34 | * 35 | * @param string $text Text to strip tags from. 36 | * @param bool $remove_breaks Whether to remove line breaks. 37 | * 38 | * @return string 39 | */ 40 | public function safe_strip_tags( $text, $remove_breaks = false ) { 41 | if ( is_null( $text ) ) { 42 | return ''; 43 | } 44 | 45 | if ( ! is_scalar( $text ) ) { 46 | return ''; 47 | } 48 | 49 | $text = preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $text ); 50 | $text = strip_tags( $text ); 51 | 52 | if ( $remove_breaks ) { 53 | $text = preg_replace( '/[\r\n\t ]+/', ' ', $text ); 54 | } 55 | 56 | return trim( $text ); 57 | } 58 | 59 | /** 60 | * Truncate a string of HTML to a certain number of words while preserving HTML tags. 61 | * 62 | * This function breaks the HTML into words and tags, truncates the words, and then 63 | * reassembles the HTML. 64 | * 65 | * @param string $html HTML string to truncate. 66 | * @param int $words Number of words to truncate to. 67 | * 68 | * @return string 69 | */ 70 | public function truncate_words( $html, $words ) { 71 | // First lets check if we need to truncate at all. 72 | $stripped_html_word_count = str_word_count( $this->safe_strip_tags( $html ) ); 73 | 74 | if ( $stripped_html_word_count <= $words ) { 75 | return $html; 76 | } 77 | 78 | $parsed = $this->parser->loadHTML( $html ); 79 | 80 | $truncated_html = ''; 81 | $word_count = 0; 82 | 83 | foreach ( $parsed->childNodes as $node ) { 84 | if ( XML_TEXT_NODE === $node->nodeType ) { 85 | $words_in_node = preg_split( '/\s+/', $node->textContent, -1, PREG_SPLIT_NO_EMPTY ); 86 | 87 | foreach ( $words_in_node as $word ) { 88 | if ( $word_count < $words ) { 89 | $truncated_html .= $word . ' '; 90 | } 91 | 92 | ++$word_count; 93 | } 94 | } else { 95 | $truncated_html .= $this->parser->saveHTML( $node ); 96 | } 97 | 98 | if ( $word_count >= $words ) { 99 | break; 100 | } 101 | } 102 | 103 | $truncated_html = trim( $truncated_html ); 104 | 105 | if ( '>' === substr( $truncated_html, -1 ) ) { 106 | $truncated_html = substr( $truncated_html, 0, strrpos( $truncated_html, '<' ) ); 107 | } 108 | 109 | return $truncated_html; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace Chophper; 11 | 12 | if ( function_exists( 'grapheme_strlen' ) ) { 13 | /** 14 | * Get the length of a string 15 | * 16 | * @param string $str The string being measured for length. 17 | * 18 | * @return int 19 | */ 20 | function ht_strlen( $str ) { 21 | return grapheme_strlen( $str ); 22 | } 23 | 24 | /** 25 | * Get part of string 26 | * 27 | * @param string $str The input string. Must be one character or longer. 28 | * @param int $from Start position in $str. If $from is non-negative, the returned string will start at the $from'th position in $str, counting from zero. If $from is negative, the returned string will start at the $from'th character from the end of string. 29 | * @param int $to If $to is given, the string returned will contain at most $to characters beginning from $from (depending on the length of $str). 30 | * 31 | * @return string 32 | */ 33 | function ht_substr( $str, $from, $to = 2147483647 ) { 34 | return grapheme_substr( $str, $from, $to ); 35 | } 36 | } elseif ( function_exists( 'mb_strlen' ) ) { 37 | /** 38 | * Get the length of a string 39 | * 40 | * @param string $str The string being measured for length. 41 | * 42 | * @return int 43 | */ 44 | function ht_strlen( $str ) { 45 | return mb_strlen( $str ); 46 | } 47 | 48 | /** 49 | * Get part of string 50 | * 51 | * @param string $str The input string. Must be one character or longer. 52 | * @param int $from Start position in $str. If $from is non-negative, the returned string will start at the $from'th position in $str, counting from zero. If $from is negative, the returned string will start at the $from'th character from the end of string. 53 | * @param int $to If $to is given, the string returned will contain at most $to characters beginning from $from (depending on the length of $str). 54 | * 55 | * @return string 56 | */ 57 | function ht_substr( $str, $from, $to = 2147483647 ) { 58 | return mb_substr( $str, $from, $to ); 59 | } 60 | } elseif ( function_exists( 'iconv_strlen' ) ) { 61 | /** 62 | * Get the length of a string 63 | * 64 | * @param string $str The string being measured for length. 65 | * 66 | * @return int 67 | */ 68 | function ht_strlen( $str ) { 69 | return iconv_strlen( $str ); 70 | } 71 | 72 | /** 73 | * Get part of string 74 | * 75 | * @param string $str The input string. Must be one character or longer. 76 | * @param int $from Start position in $str. If $from is non-negative, the returned string will start at the $from'th position in $str, counting from zero. If $from is negative, the returned string will start at the $from'th character from the end of string. 77 | * @param int $to If $to is given, the string returned will contain at most $to characters beginning from $from (depending on the length of $str). 78 | * 79 | * @return string 80 | */ 81 | function ht_substr( $str, $from, $to = 2147483647 ) { 82 | return iconv_substr( $str, $from, $to ); 83 | } 84 | } else { 85 | /** 86 | * Get the length of a string 87 | * 88 | * @param string $str The string being measured for length. 89 | * 90 | * @return int 91 | */ 92 | function ht_strlen( $str ) { 93 | return strlen( $str ); 94 | } 95 | 96 | /** 97 | * Get part of string 98 | * 99 | * @param string $str The input string. Must be one character or longer. 100 | * @param int $from Start position in $str. If $from is non-negative, the returned string will start at the $from'th position in $str, counting from zero. If $from is negative, the returned string will start at the $from'th character from the end of string. 101 | * @param int $to If $to is given, the string returned will contain at most $to characters beginning from $from (depending on the length of $str). 102 | * 103 | * @return string 104 | */ 105 | function ht_substr( $str, $from, $to = 2147483647 ) { 106 | return substr( $str, $from, $to ); 107 | } 108 | } 109 | 110 | if ( function_exists( 'mb_strtolower' ) ) { 111 | /** 112 | * Make a string lowercase 113 | * 114 | * @param string $str The string being lowercased. 115 | * 116 | * @return string 117 | */ 118 | function ht_strtolower( $str ) { 119 | return mb_strtolower( $str ); 120 | } 121 | 122 | /** 123 | * Make a string uppercase 124 | * 125 | * @param string $str The string being uppercased. 126 | * 127 | * @return string 128 | */ 129 | function ht_strtoupper( $str ) { 130 | return mb_strtoupper( $str ); 131 | } 132 | } else { 133 | /** 134 | * Make a string lowercase 135 | * 136 | * @param string $str The string being lowercased. 137 | * 138 | * @return string 139 | */ 140 | function ht_strtolower( $str ) { 141 | return strtolower( $str ); 142 | } 143 | 144 | /** 145 | * Make a string uppercase 146 | * 147 | * @param string $str The string being uppercased. 148 | * 149 | * @return string 150 | */ 151 | function ht_strtoupper( $str ) { 152 | return strtoupper( $str ); 153 | } 154 | } 155 | --------------------------------------------------------------------------------