mirror of
https://github.com/j0k3r/php-readability.git
synced 2026-09-27 06:26:17 +00:00
Compare commits
92
Commits
2.0.3
...
3e159d4e24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e159d4e24 | ||
|
|
3677c91b29 | ||
|
|
c88b196ee6 | ||
|
|
ad05b00797 | ||
|
|
cbad259ba2 | ||
|
|
a3cc317102 | ||
|
|
425a42f239 | ||
|
|
9a8d33c6ba | ||
|
|
ced50c9c10 | ||
|
|
f991d80b5a | ||
|
|
1697cdbeee | ||
|
|
eb9d8d067c | ||
|
|
9052618a31 | ||
|
|
3d332b4056 | ||
|
|
6abe0c4054 | ||
|
|
00a4d5f34d | ||
|
|
959c0680a8 | ||
|
|
74c7b80b32 | ||
|
|
33b69bdb6b | ||
|
|
f8aabce4e0 | ||
|
|
2e9d05c239 | ||
|
|
85fbb6cbf3 | ||
|
|
67a2b07505 | ||
|
|
b71d73f0e0 | ||
|
|
1dc5f6bc78 | ||
|
|
f19d7ba19e | ||
|
|
a93488d727 | ||
|
|
ac24ef54a4 | ||
|
|
76547fef78 | ||
|
|
228bc7ee1d | ||
|
|
35b87585e7 | ||
|
|
4230b2d7ca | ||
|
|
85be584f94 | ||
|
|
03a960daf0 | ||
|
|
6f62bcf662 | ||
|
|
3e3114a492 | ||
|
|
f98247ed14 | ||
|
|
5a6e525ff5 | ||
|
|
0de328760a | ||
|
|
5aa9da6843 | ||
|
|
1bb7eec83c | ||
|
|
049bd07074 | ||
|
|
ca1b105f40 | ||
|
|
5ad159ebb1 | ||
|
|
1312ffbced | ||
|
|
266e36c187 | ||
|
|
a8b08d8cb2 | ||
|
|
7db754debe | ||
|
|
18bfe842f8 | ||
|
|
63a0db9109 | ||
|
|
63d6ef1a47 | ||
|
|
116b6c839a | ||
|
|
a35f00ebb3 | ||
|
|
0f9f971556 | ||
|
|
fd1eaf61a7 | ||
|
|
3e40d78a67 | ||
|
|
03533f5e4f | ||
|
|
009b4ab9b8 | ||
|
|
f9e73fb49a | ||
|
|
3042990efc | ||
|
|
8b89d70b1a | ||
|
|
3e9b15db46 | ||
|
|
7413a38ff0 | ||
|
|
a18cd0f2a9 | ||
|
|
efbbc86df9 | ||
|
|
541fab34a0 | ||
|
|
90869d877e | ||
|
|
c7208f6ad2 | ||
|
|
4258559b8a | ||
|
|
1ac761d708 | ||
|
|
d3053fbce4 | ||
|
|
4c929754e9 | ||
|
|
1d7cdf3a12 | ||
|
|
f825dcf55a | ||
|
|
9a9373de4b | ||
|
|
d454c3a462 | ||
|
|
8b1ef07401 | ||
|
|
5885dbbe78 | ||
|
|
6947999782 | ||
|
|
da755013aa | ||
|
|
5b9551d1e3 | ||
|
|
c7b10dcc45 | ||
|
|
80adfe870b | ||
|
|
cb6b6ac577 | ||
|
|
677f3f096e | ||
|
|
29122763db | ||
|
|
89d3b74259 | ||
|
|
e792644fe8 | ||
|
|
648d8c605b | ||
|
|
f28191a728 | ||
|
|
2103853a1b | ||
|
|
7f4c6cfcbd |
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -11,16 +11,16 @@ on:
|
||||
jobs:
|
||||
coding-standards:
|
||||
name: "CS Fixer & PHPStan"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php:
|
||||
- "7.2"
|
||||
- "7.4"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--optimize-autoloader --prefer-dist"
|
||||
|
||||
|
||||
@@ -14,22 +14,22 @@ env:
|
||||
jobs:
|
||||
phpunit:
|
||||
name: "PHPUnit (PHP ${{ matrix.php }})"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php:
|
||||
- "7.2"
|
||||
- "7.3"
|
||||
- "7.4"
|
||||
- "8.0"
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
- "8.5"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--optimize-autoloader --prefer-dist"
|
||||
|
||||
@@ -60,16 +60,16 @@ jobs:
|
||||
|
||||
phpunit-coverage:
|
||||
name: "PHPUnit coverage (PHP ${{ matrix.php }})"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php:
|
||||
- "7.4"
|
||||
- "8.0"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--optimize-autoloader --prefer-dist"
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
run: "php vendor/bin/simple-phpunit -v --coverage-clover build/logs/clover.xml"
|
||||
|
||||
- name: "Retrieve Coveralls phar"
|
||||
run: "wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.2/php-coveralls.phar"
|
||||
run: "wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.7.0/php-coveralls.phar"
|
||||
|
||||
- name: "Enable Coveralls phar"
|
||||
run: "chmod +x php-coveralls.phar"
|
||||
@@ -111,16 +111,16 @@ jobs:
|
||||
|
||||
phpunit-lowest:
|
||||
name: "PHPUnit lowest deps (PHP ${{ matrix.php }})"
|
||||
runs-on: "ubuntu-20.04"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php:
|
||||
- "7.2"
|
||||
- "7.4"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v3"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v2"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--optimize-autoloader --prefer-dist"
|
||||
dependency-versions: "lowest"
|
||||
|
||||
@@ -4,3 +4,4 @@ composer.lock
|
||||
.php_cs.cache
|
||||
.php-cs-fixer.cache
|
||||
.phpunit.result.cache
|
||||
phpstan.neon
|
||||
|
||||
@@ -26,6 +26,12 @@ return (new PhpCsFixer\Config())
|
||||
'strict_comparison' => true,
|
||||
'strict_param' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
// Pulled in by @Symfony:risky but we still support PHP 7.4
|
||||
'modernize_strpos' => false,
|
||||
// Pulled in by @Symfony, we cannot add property types until we bump PHP to ≥ 7.4
|
||||
'no_null_property_initialization' => false,
|
||||
// @Symfony:risky started removing them but we will probably want to go the opposite direction.
|
||||
'declare_strict_types' => false,
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
|
||||
+9
-6
@@ -24,18 +24,18 @@
|
||||
"role": "Developer (original JS version)"
|
||||
}],
|
||||
"require": {
|
||||
"php": ">=7.2.0",
|
||||
"php": ">=7.4.0",
|
||||
"ext-mbstring": "*",
|
||||
"psr/log": "^1.0.1 || ^2.0 || ^3.0",
|
||||
"masterminds/html5": "^2.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "3.95.24",
|
||||
"monolog/monolog": "^1.24|^2.1",
|
||||
"symfony/phpunit-bridge": "^4.4|^5.3|^6.0",
|
||||
"phpstan/phpstan": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"rector/rector": "^0.15.0"
|
||||
"symfony/phpunit-bridge": "^4.4|^5.3|^6.0|^7.0",
|
||||
"phpstan/phpstan": "2.2.13",
|
||||
"phpstan/phpstan-phpunit": "2.0.18",
|
||||
"rector/rector": "2.6.6"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-tidy": "Used to clean up given HTML and to avoid problems with bad HTML structure."
|
||||
@@ -46,6 +46,9 @@
|
||||
"autoload-dev": {
|
||||
"psr-4": { "Tests\\Readability\\": "tests/" }
|
||||
},
|
||||
"config": {
|
||||
"lock": false
|
||||
},
|
||||
"scripts": {
|
||||
"fix": "php-cs-fixer fix --verbose --diff",
|
||||
"phpstan": "phpstan analyze --memory-limit 512M",
|
||||
|
||||
@@ -8,8 +8,6 @@ parameters:
|
||||
bootstrapFiles:
|
||||
- vendor/bin/.phpunit/phpunit/vendor/autoload.php
|
||||
|
||||
checkMissingIterableValueType: false
|
||||
|
||||
includes:
|
||||
- vendor/phpstan/phpstan-phpunit/extension.neon
|
||||
- vendor/phpstan/phpstan-phpunit/rules.neon
|
||||
+7
-20
@@ -3,29 +3,16 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Core\ValueObject\PhpVersion;
|
||||
use Rector\Set\ValueObject\LevelSetList;
|
||||
|
||||
return static function (RectorConfig $rectorConfig): void {
|
||||
// paths to refactor; solid alternative to CLI arguments
|
||||
$rectorConfig->paths([
|
||||
return RectorConfig::configure()
|
||||
->withPaths([
|
||||
__DIR__ . '/src',
|
||||
__DIR__ . '/tests',
|
||||
]);
|
||||
|
||||
// Path to phpstan with extensions, that PHPSTan in Rector uses to determine types
|
||||
$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');
|
||||
|
||||
$rectorConfig->bootstrapFiles([
|
||||
])
|
||||
->withBootstrapFiles([
|
||||
__DIR__ . '/vendor/bin/.phpunit/phpunit/vendor/autoload.php',
|
||||
__DIR__ . '/vendor/autoload.php',
|
||||
]);
|
||||
|
||||
// Define what rule sets will be applied
|
||||
$rectorConfig->sets([
|
||||
LevelSetList::UP_TO_PHP_72,
|
||||
]);
|
||||
|
||||
// is your PHP version different from the one your refactor to?
|
||||
$rectorConfig->phpVersion(PhpVersion::PHP_72);
|
||||
};
|
||||
])
|
||||
->withSets([LevelSetList::UP_TO_PHP_74])
|
||||
;
|
||||
|
||||
@@ -43,7 +43,7 @@ class JSLikeHTMLElement extends \DOMElement
|
||||
* $div->innerHTML = '<h2>Chapter 2</h2><p>The story begins...</p>';
|
||||
* ```
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
public function __set(string $name, string $value): void
|
||||
{
|
||||
if ('innerHTML' !== $name) {
|
||||
$trace = debug_backtrace();
|
||||
@@ -108,7 +108,7 @@ class JSLikeHTMLElement extends \DOMElement
|
||||
* $string = $div->innerHTML;
|
||||
* ```
|
||||
*/
|
||||
public function __get($name)
|
||||
public function __get(string $name): string
|
||||
{
|
||||
if ('innerHTML' === $name) {
|
||||
$inner = '';
|
||||
@@ -124,20 +124,22 @@ class JSLikeHTMLElement extends \DOMElement
|
||||
|
||||
$trace = debug_backtrace();
|
||||
trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], \E_USER_NOTICE);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
public function __toString(): string
|
||||
{
|
||||
return '[' . $this->tagName . ']';
|
||||
}
|
||||
|
||||
public function getInnerHtml()
|
||||
public function getInnerHtml(): string
|
||||
{
|
||||
return $this->__get('innerHTML');
|
||||
}
|
||||
|
||||
public function setInnerHtml($value)
|
||||
public function setInnerHtml(string $value): void
|
||||
{
|
||||
return $this->__set('innerHTML', $value);
|
||||
$this->__set('innerHTML', $value);
|
||||
}
|
||||
}
|
||||
|
||||
+167
-123
@@ -88,7 +88,7 @@ class Readability implements LoggerAwareInterface
|
||||
'enclose-text' => true,
|
||||
'merge-divs' => true,
|
||||
// 'merge-spans' => true,
|
||||
'input-encoding' => '????',
|
||||
'input-encoding' => 'utf8',
|
||||
'output-encoding' => 'utf8',
|
||||
'hide-comments' => true,
|
||||
];
|
||||
@@ -142,7 +142,7 @@ class Readability implements LoggerAwareInterface
|
||||
* @param string $parser Which parser to use for turning raw HTML into a DOMDocument
|
||||
* @param bool $useTidy Use tidy
|
||||
*/
|
||||
public function __construct(string $html, string $url = null, string $parser = 'libxml', bool $useTidy = true)
|
||||
public function __construct(string $html, ?string $url = null, string $parser = 'libxml', bool $useTidy = true)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->html = $html;
|
||||
@@ -213,7 +213,9 @@ class Readability implements LoggerAwareInterface
|
||||
*/
|
||||
public function init(): bool
|
||||
{
|
||||
if (!isset($this->dom)) {
|
||||
$this->loadHtml();
|
||||
}
|
||||
|
||||
if (!isset($this->dom->documentElement)) {
|
||||
return false;
|
||||
@@ -302,8 +304,7 @@ class Readability implements LoggerAwareInterface
|
||||
$articleLinks = $articleContent->getElementsByTagName('a');
|
||||
$linkCount = 0;
|
||||
|
||||
for ($i = 0; $i < $articleLinks->length; ++$i) {
|
||||
$articleLink = $articleLinks->item($i);
|
||||
foreach ($articleLinks as $articleLink) {
|
||||
$footnoteLink = $articleLink->cloneNode(true);
|
||||
$refLink = $this->dom->createElement('a');
|
||||
$footnote = $this->dom->createElement('li');
|
||||
@@ -383,8 +384,8 @@ class Readability implements LoggerAwareInterface
|
||||
|
||||
// Remove service data-candidate attribute.
|
||||
$elems = $xpath->query('.//*[@data-candidate]', $articleContent);
|
||||
for ($i = $elems->length - 1; $i >= 0; --$i) {
|
||||
$elems->item($i)->removeAttribute('data-candidate');
|
||||
foreach ($elems as $elem) {
|
||||
$elem->removeAttribute('data-candidate');
|
||||
}
|
||||
|
||||
// Clean out junk from the article content.
|
||||
@@ -520,11 +521,12 @@ class Readability implements LoggerAwareInterface
|
||||
$textLength = mb_strlen($this->getInnerText($e, true, true));
|
||||
$linkLength = 0;
|
||||
|
||||
for ($dRe = $this->domainRegExp, $i = 0, $il = $links->length; $i < $il; ++$i) {
|
||||
if ($excludeExternal && $dRe && !preg_match($dRe, $links->item($i)->getAttribute('href'))) {
|
||||
$dRe = $this->domainRegExp;
|
||||
foreach ($links as $link) {
|
||||
if ($excludeExternal && $dRe && !preg_match($dRe, $link->getAttribute('href'))) {
|
||||
continue;
|
||||
}
|
||||
$linkLength += mb_strlen($this->getInnerText($links->item($i)));
|
||||
$linkLength += mb_strlen($this->getInnerText($link));
|
||||
}
|
||||
|
||||
if ($textLength > 0 && $linkLength > 0) {
|
||||
@@ -586,7 +588,7 @@ class Readability implements LoggerAwareInterface
|
||||
}
|
||||
|
||||
// Then check the elements inside this element for the same.
|
||||
if (preg_match($this->regexps['media'], $targetList->item($y)->getInnerHTML())) {
|
||||
if (preg_match($this->regexps['media'], $currentItem->getInnerHTML())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -640,15 +642,15 @@ class Readability implements LoggerAwareInterface
|
||||
$embedCount = 0;
|
||||
$embeds = $node->getElementsByTagName('embed');
|
||||
|
||||
for ($ei = 0, $il = $embeds->length; $ei < $il; ++$ei) {
|
||||
if (preg_match($this->regexps['media'], $embeds->item($ei)->getAttribute('src'))) {
|
||||
foreach ($embeds as $embed) {
|
||||
if (preg_match($this->regexps['media'], $embed->getAttribute('src'))) {
|
||||
++$embedCount;
|
||||
}
|
||||
}
|
||||
|
||||
$embeds = $node->getElementsByTagName('iframe');
|
||||
for ($ei = 0, $il = $embeds->length; $ei < $il; ++$ei) {
|
||||
if (preg_match($this->regexps['media'], $embeds->item($ei)->getAttribute('src'))) {
|
||||
foreach ($embeds as $embed) {
|
||||
if (preg_match($this->regexps['media'], $embed->getAttribute('src'))) {
|
||||
++$embedCount;
|
||||
}
|
||||
}
|
||||
@@ -668,10 +670,10 @@ class Readability implements LoggerAwareInterface
|
||||
$this->logger->debug(' content length less than 6 chars, 0 embeds and either 0 images or more than 2 images');
|
||||
$toRemove = true;
|
||||
} elseif (!$isList && $weight < 25 && $linkDensity > 0.25) {
|
||||
$this->logger->debug(' weight is ' . $weight . ' < 25 and link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.25');
|
||||
$this->logger->debug(' weight is ' . $weight . ' < 25 and link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.25');
|
||||
$toRemove = true;
|
||||
} elseif ($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
|
||||
$this->logger->debug(' more than 2 links and weight is ' . $weight . ' > 25 but link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.5');
|
||||
$this->logger->debug(' more than 2 links and weight is ' . $weight . ' > 25 but link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.5');
|
||||
$toRemove = true;
|
||||
} elseif ($embedCount > 3) {
|
||||
$this->logger->debug(' more than 3 embeds');
|
||||
@@ -691,10 +693,10 @@ class Readability implements LoggerAwareInterface
|
||||
$this->logger->debug(' content length less than 10 chars and 0 images, or more than 2 images');
|
||||
$toRemove = true;
|
||||
} elseif (!$isList && $weight < 25 && $linkDensity > 0.2) {
|
||||
$this->logger->debug(' weight is ' . $weight . ' lower than 0 and link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.2');
|
||||
$this->logger->debug(' weight is ' . $weight . ' lower than 0 and link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.2');
|
||||
$toRemove = true;
|
||||
} elseif ($weight >= 25 && $linkDensity > 0.5) {
|
||||
$this->logger->debug(' weight above 25 but link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.5');
|
||||
$this->logger->debug(' weight above 25 but link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.5');
|
||||
$toRemove = true;
|
||||
} elseif ((1 === $embedCount && $contentLength < 75) || $embedCount > 1) {
|
||||
$this->logger->debug(' 1 embed and content length smaller than 75 chars, or more than one embed');
|
||||
@@ -719,8 +721,9 @@ class Readability implements LoggerAwareInterface
|
||||
$headers = $e->getElementsByTagName('h' . $headerIndex);
|
||||
|
||||
for ($i = $headers->length - 1; $i >= 0; --$i) {
|
||||
if ($this->getWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
|
||||
$headers->item($i)->parentNode->removeChild($headers->item($i));
|
||||
$header = $headers->item($i);
|
||||
if ($this->getWeight($header) < 0 || $this->getLinkDensity($header) > 0.33) {
|
||||
$header->parentNode->removeChild($header);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -739,7 +742,7 @@ class Readability implements LoggerAwareInterface
|
||||
*/
|
||||
public function addFlag(int $flag): void
|
||||
{
|
||||
$this->flags = $this->flags | $flag;
|
||||
$this->flags |= $flag;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -747,7 +750,77 @@ class Readability implements LoggerAwareInterface
|
||||
*/
|
||||
public function removeFlag(int $flag): void
|
||||
{
|
||||
$this->flags = $this->flags & ~$flag;
|
||||
$this->flags &= ~$flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML in a DOMDocument.
|
||||
* Apply Pre filters
|
||||
* Cleanup HTML using Tidy (or not).
|
||||
*/
|
||||
public function loadHtml(): void
|
||||
{
|
||||
$this->original_html = $this->html;
|
||||
|
||||
$this->logger->debug('Parsing URL: ' . $this->url);
|
||||
|
||||
if ($this->url) {
|
||||
$host = parse_url($this->url, \PHP_URL_HOST);
|
||||
if (null !== $host) {
|
||||
$this->domainRegExp = '/' . strtr((string) preg_replace('/www\d*\./', '', $host), ['.' => '\.']) . '/';
|
||||
}
|
||||
}
|
||||
|
||||
mb_internal_encoding('UTF-8');
|
||||
mb_http_output('UTF-8');
|
||||
mb_regex_encoding('UTF-8');
|
||||
|
||||
// HACK: dirty cleanup to replace some stuff; shouldn't use regexps with HTML but well...
|
||||
if (!$this->flagIsActive(self::FLAG_DISABLE_PREFILTER)) {
|
||||
foreach ($this->pre_filters as $search => $replace) {
|
||||
$this->html = preg_replace($search, $replace, $this->html);
|
||||
}
|
||||
unset($search, $replace);
|
||||
}
|
||||
|
||||
if ('' === trim($this->html)) {
|
||||
$this->html = '<html></html>';
|
||||
}
|
||||
|
||||
/*
|
||||
* Use tidy (if it exists).
|
||||
* This fixes problems with some sites which would otherwise trouble DOMDocument's HTML parsing.
|
||||
* Although sometimes it makes matters worse, which is why there is an option to disable it.
|
||||
*/
|
||||
if ($this->useTidy) {
|
||||
$this->logger->debug('Tidying document');
|
||||
|
||||
$tidy = tidy_repair_string($this->html, $this->tidy_config, 'UTF8');
|
||||
if (false !== $tidy && $this->html !== $tidy) {
|
||||
$this->tidied = true;
|
||||
$this->html = $tidy;
|
||||
$this->html = preg_replace('/[\r\n]+/is', "\n", $this->html);
|
||||
}
|
||||
unset($tidy);
|
||||
}
|
||||
|
||||
$this->html = self::entitizeNonAscii((string) $this->html);
|
||||
|
||||
if ('html5lib' === $this->parser || 'html5' === $this->parser) {
|
||||
$this->dom = (new HTML5())->loadHTML($this->html);
|
||||
}
|
||||
|
||||
if ('libxml' === $this->parser) {
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$this->dom = new \DOMDocument();
|
||||
$this->dom->preserveWhiteSpace = false;
|
||||
$this->dom->loadHTML($this->html, \LIBXML_NOBLANKS | \LIBXML_COMPACT | \LIBXML_NOERROR);
|
||||
|
||||
libxml_use_internal_errors(false);
|
||||
}
|
||||
|
||||
$this->dom->registerNodeClass(\DOMElement::class, JSLikeHTMLElement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,12 +885,14 @@ class Readability implements LoggerAwareInterface
|
||||
// Remove all style tags in head.
|
||||
$styleTags = $this->dom->getElementsByTagName('style');
|
||||
for ($i = $styleTags->length - 1; $i >= 0; --$i) {
|
||||
$styleTags->item($i)->parentNode->removeChild($styleTags->item($i));
|
||||
$styleTag = $styleTags->item($i);
|
||||
$styleTag->parentNode->removeChild($styleTag);
|
||||
}
|
||||
|
||||
$linkTags = $this->dom->getElementsByTagName('link');
|
||||
for ($i = $linkTags->length - 1; $i >= 0; --$i) {
|
||||
$linkTags->item($i)->parentNode->removeChild($linkTags->item($i));
|
||||
$linkTag = $linkTags->item($i);
|
||||
$linkTag->parentNode->removeChild($linkTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,11 +968,9 @@ class Readability implements LoggerAwareInterface
|
||||
* Using a variety of metrics (content score, classname, element types), find the content that is
|
||||
* most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
|
||||
*
|
||||
* @param \DOMElement $page
|
||||
*
|
||||
* @return \DOMElement|false
|
||||
*/
|
||||
protected function grabArticle(\DOMElement $page = null)
|
||||
protected function grabArticle(?\DOMElement $page = null)
|
||||
{
|
||||
if (!$page) {
|
||||
$page = $this->dom;
|
||||
@@ -933,9 +1006,9 @@ class Readability implements LoggerAwareInterface
|
||||
// Remove unlikely candidates
|
||||
$unlikelyMatchString = $node->getAttribute('class') . ' ' . $node->getAttribute('id') . ' ' . $node->getAttribute('style');
|
||||
|
||||
if (mb_strlen($unlikelyMatchString) > 3 && // don't process "empty" strings
|
||||
preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
|
||||
!preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
|
||||
if (mb_strlen($unlikelyMatchString) > 3 // don't process "empty" strings
|
||||
&& preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString)
|
||||
&& !preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
|
||||
) {
|
||||
$this->logger->debug('Removing unlikely candidate (using conf) ' . $node->getNodePath() . ' by "' . $unlikelyMatchString . '"');
|
||||
$node->parentNode->removeChild($node);
|
||||
@@ -1002,7 +1075,11 @@ class Readability implements LoggerAwareInterface
|
||||
}
|
||||
|
||||
if ($this->hasSingleTagInsideElement($node, 'p') && $this->getLinkDensity($node) < 0.25) {
|
||||
$newNode = $node->childNodes->item(0);
|
||||
// In some cases when tidy is disabled the first item may not be a DOMElement so we apply a filter
|
||||
$newNode = array_values(array_filter(
|
||||
iterator_to_array($node->childNodes),
|
||||
static fn ($childNode) => $childNode instanceof \DOMElement
|
||||
))[0];
|
||||
$node->parentNode->replaceChild($newNode, $node);
|
||||
$nodesToScore[] = $newNode;
|
||||
}
|
||||
@@ -1017,15 +1094,15 @@ class Readability implements LoggerAwareInterface
|
||||
* A score is determined by things like number of commas, class names, etc.
|
||||
* Maybe eventually link density.
|
||||
*/
|
||||
for ($pt = 0, $scored = \count($nodesToScore); $pt < $scored; ++$pt) {
|
||||
$ancestors = $this->getAncestors($nodesToScore[$pt], 5);
|
||||
foreach ($nodesToScore as $nodeToScore) {
|
||||
$ancestors = $this->getAncestors($nodeToScore, 5);
|
||||
|
||||
// No parent node? Move on...
|
||||
if (0 === \count($ancestors)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$innerText = $this->getInnerText($nodesToScore[$pt]);
|
||||
$innerText = $this->getInnerText($nodeToScore);
|
||||
|
||||
// If this paragraph is less than MIN_PARAGRAPH_LENGTH (default:20) characters, don't even count it.
|
||||
if (mb_strlen($innerText) < self::MIN_PARAGRAPH_LENGTH) {
|
||||
@@ -1078,11 +1155,6 @@ class Readability implements LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
$candidates = $xpath->query('.//*[not(self::body) and (@class or @id or @style) and ((number(@readability) < 40) or not(@readability))]', $page->documentElement);
|
||||
|
||||
for ($c = $candidates->length - 1; $c >= 0; --$c) {
|
||||
$node = $candidates->item($c);
|
||||
}
|
||||
unset($candidates);
|
||||
}
|
||||
|
||||
@@ -1120,9 +1192,11 @@ class Readability implements LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
$topCandidates = array_filter($topCandidates, function ($v, $idx) {
|
||||
return 0 === $idx || null !== $v;
|
||||
}, \ARRAY_FILTER_USE_BOTH);
|
||||
$topCandidates = array_filter(
|
||||
$topCandidates,
|
||||
static fn ($v, $idx) => 0 === $idx || null !== $v,
|
||||
\ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
$topCandidate = $topCandidates[0];
|
||||
|
||||
/*
|
||||
@@ -1231,11 +1305,6 @@ class Readability implements LoggerAwareInterface
|
||||
$parentOfTopCandidate = $topCandidate->parentNode;
|
||||
$siblingNodes = $parentOfTopCandidate->childNodes;
|
||||
|
||||
if (0 === $siblingNodes->length) {
|
||||
$siblingNodes = new \stdClass();
|
||||
$siblingNodes->length = 0;
|
||||
}
|
||||
|
||||
for ($s = 0, $sl = $siblingNodes->length; $s < $sl; ++$s) {
|
||||
$siblingNode = $siblingNodes->item($s);
|
||||
$siblingNodeName = $siblingNode->nodeName;
|
||||
@@ -1378,73 +1447,6 @@ class Readability implements LoggerAwareInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML in a DOMDocument.
|
||||
* Apply Pre filters
|
||||
* Cleanup HTML using Tidy (or not).
|
||||
*/
|
||||
private function loadHtml(): void
|
||||
{
|
||||
$this->original_html = $this->html;
|
||||
|
||||
$this->logger->debug('Parsing URL: ' . $this->url);
|
||||
|
||||
if ($this->url) {
|
||||
$this->domainRegExp = '/' . strtr((string) preg_replace('/www\d*\./', '', (string) parse_url($this->url, \PHP_URL_HOST)), ['.' => '\.']) . '/';
|
||||
}
|
||||
|
||||
mb_internal_encoding('UTF-8');
|
||||
mb_http_output('UTF-8');
|
||||
mb_regex_encoding('UTF-8');
|
||||
|
||||
// HACK: dirty cleanup to replace some stuff; shouldn't use regexps with HTML but well...
|
||||
if (!$this->flagIsActive(self::FLAG_DISABLE_PREFILTER)) {
|
||||
foreach ($this->pre_filters as $search => $replace) {
|
||||
$this->html = preg_replace($search, $replace, $this->html);
|
||||
}
|
||||
unset($search, $replace);
|
||||
}
|
||||
|
||||
if ('' === trim($this->html)) {
|
||||
$this->html = '<html></html>';
|
||||
}
|
||||
|
||||
/*
|
||||
* Use tidy (if it exists).
|
||||
* This fixes problems with some sites which would otherwise trouble DOMDocument's HTML parsing.
|
||||
* Although sometimes it makes matters worse, which is why there is an option to disable it.
|
||||
*/
|
||||
if ($this->useTidy) {
|
||||
$this->logger->debug('Tidying document');
|
||||
|
||||
$tidy = tidy_repair_string($this->html, $this->tidy_config, 'UTF8');
|
||||
if (false !== $tidy && $this->html !== $tidy) {
|
||||
$this->tidied = true;
|
||||
$this->html = $tidy;
|
||||
$this->html = preg_replace('/[\r\n]+/is', "\n", $this->html);
|
||||
}
|
||||
unset($tidy);
|
||||
}
|
||||
|
||||
$this->html = '<meta charset="utf-8">' . (string) $this->html;
|
||||
|
||||
if ('html5lib' === $this->parser || 'html5' === $this->parser) {
|
||||
$this->dom = (new HTML5())->loadHTML($this->html);
|
||||
}
|
||||
|
||||
if ('libxml' === $this->parser) {
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$this->dom = new \DOMDocument();
|
||||
$this->dom->preserveWhiteSpace = false;
|
||||
$this->dom->loadHTML($this->html, \LIBXML_NOBLANKS | \LIBXML_COMPACT | \LIBXML_NOERROR);
|
||||
|
||||
libxml_use_internal_errors(false);
|
||||
}
|
||||
|
||||
$this->dom->registerNodeClass(\DOMElement::class, \Readability\JSLikeHTMLElement::class);
|
||||
}
|
||||
|
||||
private function getAncestors(\DOMElement $node, int $maxDepth = 0): array
|
||||
{
|
||||
$ancestors = [];
|
||||
@@ -1464,21 +1466,45 @@ class Readability implements LoggerAwareInterface
|
||||
{
|
||||
return \XML_TEXT_NODE === $node->nodeType
|
||||
|| \in_array(strtoupper($node->nodeName), $this->phrasingElements, true)
|
||||
|| (\in_array(strtoupper($node->nodeName), ['A', 'DEL', 'INS'], true) && !\in_array(false, array_map(function ($c) {
|
||||
return $this->isPhrasingContent($c);
|
||||
}, iterator_to_array($node->childNodes)), true));
|
||||
|| (
|
||||
\in_array(strtoupper($node->nodeName), ['A', 'DEL', 'INS'], true)
|
||||
&& !\in_array(
|
||||
false,
|
||||
array_map(
|
||||
fn ($c) => $this->isPhrasingContent($c),
|
||||
iterator_to_array($node->childNodes)
|
||||
),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `$node` has only whitespace and a single element with `$tag` for the tag name.
|
||||
* Returns false if `$node` contains non-empty text nodes
|
||||
* or if it contains no element with given tag or more than 1 element.
|
||||
*/
|
||||
private function hasSingleTagInsideElement(\DOMElement $node, string $tag): bool
|
||||
{
|
||||
if (1 !== $node->childNodes->length || $node->childNodes->item(0)->nodeName !== $tag) {
|
||||
$childNodes = iterator_to_array($node->childNodes);
|
||||
$children = array_filter($childNodes, static fn ($childNode) => $childNode instanceof \DOMElement);
|
||||
|
||||
// array_first() has been added in PHP 8.5, failing back for older versions
|
||||
if (!\function_exists('array_first')) {
|
||||
$firstChild = $children ? $children[array_key_first($children)] : null;
|
||||
} else {
|
||||
$firstChild = array_first($children);
|
||||
}
|
||||
|
||||
// There should be exactly 1 element child with given tag
|
||||
if (1 !== \count($children) || $firstChild->nodeName !== $tag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$a = array_filter(iterator_to_array($node->childNodes), function ($childNode) {
|
||||
return $childNode instanceof \DOMText &&
|
||||
preg_match($this->regexps['hasContent'], $this->getInnerText($childNode));
|
||||
});
|
||||
$a = array_filter(
|
||||
$childNodes,
|
||||
fn ($childNode) => $childNode instanceof \DOMText && preg_match($this->regexps['hasContent'], $this->getInnerText($childNode))
|
||||
);
|
||||
|
||||
return 0 === \count($a);
|
||||
}
|
||||
@@ -1491,9 +1517,27 @@ class Readability implements LoggerAwareInterface
|
||||
*/
|
||||
private function isNodeVisible(\DOMElement $node): bool
|
||||
{
|
||||
return !($node->hasAttribute('style')
|
||||
return !(
|
||||
$node->hasAttribute('style')
|
||||
&& preg_match($this->regexps['isNotVisible'], $node->getAttribute('style'))
|
||||
)
|
||||
&& !$node->hasAttribute('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts non-ASCII UTF-8 characters to numeric HTML entities.
|
||||
*
|
||||
* `DOMDocument::loadHTML` will parse HTML documents as ISO-8859-1 if there is no `meta[charset]` tag.
|
||||
* This means that UTF-8-encoded HTML fragments such as those coming from JSON-LD `articleBody` field would be parsed with incorrect encoding.
|
||||
*
|
||||
* @param string $html UTF-8 encoded document
|
||||
*/
|
||||
private static function entitizeNonAscii(string $html): string
|
||||
{
|
||||
$convmap = [
|
||||
0x80, 0x1FFFFF, 0, 0x10FFFF,
|
||||
];
|
||||
|
||||
return mb_encode_numericentity($html, $convmap, 'utf8', true);
|
||||
}
|
||||
}
|
||||
|
||||
+124
-30
@@ -21,10 +21,11 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testConstructDefault(): void
|
||||
{
|
||||
$readability = $this->getReadability('');
|
||||
$this->assertSame('utf8', $readability->tidy_config['input-encoding']);
|
||||
$readability->init();
|
||||
|
||||
$this->assertNull($readability->url);
|
||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
}
|
||||
|
||||
public function testConstructHtml5Parser(): void
|
||||
@@ -33,7 +34,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$readability->init();
|
||||
|
||||
$this->assertSame('http://0.0.0.0', $readability->url);
|
||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
$this->assertSame('<html/>', $readability->original_html);
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$readability->init();
|
||||
|
||||
$this->assertSame('http://0.0.0.0', $readability->url);
|
||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
$this->assertSame('<html/>', $readability->original_html);
|
||||
$this->assertTrue($readability->tidied);
|
||||
}
|
||||
@@ -60,7 +61,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertSame('', $readability->original_html);
|
||||
$this->assertFalse($readability->tidied);
|
||||
|
||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
}
|
||||
|
||||
public function testConstructSimpleWithoutTidy(): void
|
||||
@@ -69,7 +70,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$readability->init();
|
||||
|
||||
$this->assertSame('http://0.0.0.0', $readability->url);
|
||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
$this->assertSame('<html/>', $readability->original_html);
|
||||
$this->assertFalse($readability->tidied);
|
||||
}
|
||||
@@ -115,7 +116,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testInitDiv(): void
|
||||
{
|
||||
$readability = $this->getReadability('<div>' . str_repeat('This is the awesome content :)', 7) . '</div>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -129,7 +129,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithFootnotes(): void
|
||||
{
|
||||
$readability = $this->getReadability('<div>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '</div>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$readability->convertLinksToFootnotes = true;
|
||||
$res = $readability->init();
|
||||
|
||||
@@ -146,7 +145,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testStandardClean(): void
|
||||
{
|
||||
$readability = $this->getReadability('<div><h2>Title</h2>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<a href="#nofollow" rel="nofollow">will NOT be removed</a></div>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$readability->lightClean = false;
|
||||
$res = $readability->init();
|
||||
|
||||
@@ -163,7 +161,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithIframe(): void
|
||||
{
|
||||
$readability = $this->getReadability('<div><h2>Title</h2>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<p>This is an awesome text with some links, here there are <iframe src="http://youtube.com/test" href="#nofollow" rel="nofollow"></iframe><iframe>http://soundcloud.com/test</iframe></p></div>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -178,7 +175,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithArticle(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article><p>' . str_repeat('This is an awesome text with some links, here there are: the awesome', 20) . '</p><p>This is an awesome text with some links, here there are <iframe src="http://youtube.com/test" href="#nofollow" rel="nofollow"></iframe></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -193,7 +189,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithAside(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<footer><aside>' . str_repeat('<p>This is an awesome text with some links, here there are</p>', 8) . '</aside></footer></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -208,7 +203,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithClasses(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<div style="display:none">' . str_repeat('<p class="clock">This text should be removed</p>', 10) . '</div></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -223,7 +217,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithClassesWithoutLightClean(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<div style="display:none">' . str_repeat('<p class="clock">This text should be removed</p>', 10) . '</div></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$readability->lightClean = false;
|
||||
$res = $readability->init();
|
||||
|
||||
@@ -239,7 +232,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithTd(): void
|
||||
{
|
||||
$readability = $this->getReadability('<table><tr>' . str_repeat('<td><p>This is an awesome text with some links, here there are the awesome</td>', 7) . '</tr></table>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -252,7 +244,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithSameClasses(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<div class="awesomecontent">This text is also an awesome text and you should know that !</div></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -266,7 +257,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testWithScript(): void
|
||||
{
|
||||
$readability = $this->getReadability('<article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p><script>This text is also an awesome text and you should know that !</script></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -280,7 +270,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testTitle(): void
|
||||
{
|
||||
$readability = $this->getReadability('<title>this is my title</title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -294,7 +283,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testTitleWithDash(): void
|
||||
{
|
||||
$readability = $this->getReadability('<title> title2 - title3 </title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -308,7 +296,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testTitleWithDoubleDot(): void
|
||||
{
|
||||
$readability = $this->getReadability('<title> title2 : title3 </title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -322,7 +309,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testTitleTooShortUseH1(): void
|
||||
{
|
||||
$readability = $this->getReadability('<title>too short</title><h1>this is my h1 title !</h1><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -335,12 +321,12 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
|
||||
public function testAutoClosingIframeNotThrowingException(): void
|
||||
{
|
||||
$oldErrorReporting = error_reporting(\E_ALL | \E_STRICT);
|
||||
$oldErrorReporting = error_reporting(\E_ALL);
|
||||
$oldDisplayErrors = ini_set('display_errors', '1');
|
||||
// dummy function to be used to the next test
|
||||
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline, array $errcontext) {
|
||||
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
|
||||
throw new \Exception($errstr, $errno);
|
||||
}, \E_ALL | \E_STRICT);
|
||||
});
|
||||
|
||||
try {
|
||||
$data = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
@@ -369,7 +355,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
</html>';
|
||||
|
||||
$readability = $this->getReadability($data, 'http://iosgames.ru/?p=22030');
|
||||
$readability->debug = true;
|
||||
|
||||
$res = $readability->init();
|
||||
|
||||
@@ -437,7 +422,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
</html>';
|
||||
|
||||
$readability = $this->getReadability($data, 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
|
||||
$res = $readability->init();
|
||||
|
||||
@@ -474,20 +458,74 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$html = (string) file_get_contents('tests/fixtures/childNodeGoesNull.html');
|
||||
|
||||
$readability = $this->getReadability($html, 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$readability->convertLinksToFootnotes = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
}
|
||||
|
||||
public function dataForHasSingleTagInsideElement(): array
|
||||
{
|
||||
return [
|
||||
'single matching tag, no other content' => [
|
||||
'<div><p>Some text</p></div>', 'p', true,
|
||||
],
|
||||
'single matching tag with comment' => [
|
||||
'<div><!-- comment --><p>Some text</p></div>', 'p', true,
|
||||
],
|
||||
'whitespace-only text around single matching tag' => [
|
||||
'<div> <p>Some text</p> </div>', 'p', true,
|
||||
],
|
||||
'two matching tags' => [
|
||||
'<div><p>One</p><p>Two</p></div>', 'p', false,
|
||||
],
|
||||
'non-whitespace text alongside the single tag' => [
|
||||
'<div>Some text<p>One</p></div>', 'p', false,
|
||||
],
|
||||
'single tag with a different name' => [
|
||||
'<div><span>One</span></div>', 'p', false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataForHasSingleTagInsideElement
|
||||
*/
|
||||
public function testHasSingleTagInsideElement(string $html, string $tag, bool $expected): void
|
||||
{
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadHTML('<html><body>' . $html . '</body></html>');
|
||||
$node = $dom->getElementsByTagName('div')->item(0);
|
||||
|
||||
$readability = $this->getReadability('', '', 'libxml', false);
|
||||
|
||||
$method = new \ReflectionMethod($readability, 'hasSingleTagInsideElement');
|
||||
if (\PHP_VERSION_ID < 80100) {
|
||||
$method->setAccessible(true);
|
||||
}
|
||||
|
||||
$this->assertSame($expected, $method->invoke($readability, $node, $tag));
|
||||
}
|
||||
|
||||
public function testDivToPElementWithComment(): void
|
||||
{
|
||||
$text = str_repeat('Padded real article text to reach decent length for content scoring threshold here. ', 8);
|
||||
$html = '<div><!-- comment --><code>some code snippet</code> ' . $text . '</div>';
|
||||
|
||||
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', false);
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
$this->assertStringContainsString('some code snippet', $readability->getContent()->getInnerHtml());
|
||||
$this->assertStringContainsString('Padded real article text', $readability->getContent()->getInnerHtml());
|
||||
}
|
||||
|
||||
public function testKeepFootnotes(): void
|
||||
{
|
||||
// from https://www.schreibdichte.de/blog/feed-aggregator-und-spaeter-lesen-dienst-im-team
|
||||
$html = (string) file_get_contents('tests/fixtures/keepFootnotes.html');
|
||||
|
||||
$readability = $this->getReadability($html, 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -501,7 +539,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
$html = (string) file_get_contents('tests/fixtures/wipedBody.html');
|
||||
|
||||
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', false);
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
@@ -540,7 +577,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
public function testVisibleNode(string $content, bool $shouldBeVisible): void
|
||||
{
|
||||
$readability = $this->getReadability($content, 'http://0.0.0.0');
|
||||
$readability->debug = true;
|
||||
$res = $readability->init();
|
||||
|
||||
if ($shouldBeVisible) {
|
||||
@@ -550,7 +586,65 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
|
||||
}
|
||||
}
|
||||
|
||||
private function getReadability(string $html, string $url = null, string $parser = 'libxml', bool $useTidy = true): Readability
|
||||
// https://github.com/wallabag/wallabag/issues/8158
|
||||
public function testCharsetAfterTitle(): void
|
||||
{
|
||||
$readability = $this->getReadability('<!DOCTYPE html><html lang="et"><head><title>Tõde ja õigus I</title> <meta charset="utf-8"></head><body><p>See oli läinud aastasaja kolmanda veerandi lõpul. Päike lähenes silmapiirile, seistes sedavõrd madalas, et enam ei ulatunud valgustama ei mäkke ronivat hobust, kes puutelgedega vankrit vedas, ei vankril istuvat noort naist ega ka ligi kolmekümnelist meest, kes kõndis vankri kõrval.</p></body></html>', 'https://et.wikisource.org/wiki/T%C3%B5de_ja_%C3%B5igus_I/I');
|
||||
$readability->convertLinksToFootnotes = true;
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getContent());
|
||||
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getTitle());
|
||||
$this->assertSame('Tõde ja õigus I', $readability->getTitle()->getInnerHtml());
|
||||
$this->assertStringContainsString('Päike lähenes', $readability->getContent()->getInnerHtml());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: string, 1: string, 2?: bool}>
|
||||
*/
|
||||
public function dataForHtmlLang(): array
|
||||
{
|
||||
return [
|
||||
'meta' => [
|
||||
'<html lang="fr"><head><meta charset="utf-8"></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
|
||||
'fr',
|
||||
],
|
||||
'head' => [
|
||||
'<html lang="fr"><head><title>Foo</title></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
|
||||
'fr',
|
||||
],
|
||||
'headless' => [
|
||||
'<html lang="fr"><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
|
||||
'fr',
|
||||
// tidy would add <head> tag.
|
||||
false,
|
||||
],
|
||||
'fragment' => [
|
||||
'<article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article>',
|
||||
'',
|
||||
// tidy would add <html>.
|
||||
false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataForHtmlLang
|
||||
*/
|
||||
public function testHtmlLang(string $html, string $lang, bool $useTidy = true): void
|
||||
{
|
||||
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', $useTidy);
|
||||
$res = $readability->init();
|
||||
|
||||
$this->assertTrue($res);
|
||||
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
|
||||
$this->assertSame($lang, $readability->dom->documentElement->getAttribute('lang'));
|
||||
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getContent());
|
||||
$this->assertStringContainsString('êtres', $readability->getContent()->getInnerHtml());
|
||||
}
|
||||
|
||||
private function getReadability(string $html, ?string $url = null, string $parser = 'libxml', bool $useTidy = true): Readability
|
||||
{
|
||||
$readability = new Readability($html, $url, $parser, $useTidy);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user