mirror of
https://github.com/j0k3r/php-readability.git
synced 2026-09-27 14:36:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dec4514c00 | ||
|
|
00f622e9b7 | ||
|
|
7b47e2f1de | ||
|
|
c756ec067e | ||
|
|
8ab7d76cd5 | ||
|
|
149a333b40 | ||
|
|
209c404d7b | ||
|
|
6e3f2e8c0b | ||
|
|
8cbecd7e99 | ||
|
|
438699ab15 | ||
|
|
ea7a62d352 | ||
|
|
9181fd1a2c | ||
|
|
2951936e00 | ||
|
|
92965f76cf | ||
|
|
850ade16b6 | ||
|
|
dc590542f0 |
+1
-2
@@ -1,3 +1,2 @@
|
|||||||
tools:
|
tools:
|
||||||
external_code_coverage:
|
external_code_coverage: false
|
||||||
timeout: 600
|
|
||||||
|
|||||||
+15
-7
@@ -6,29 +6,37 @@ php:
|
|||||||
- 5.4
|
- 5.4
|
||||||
- 5.5
|
- 5.5
|
||||||
- 5.6
|
- 5.6
|
||||||
- nightly
|
- 7.0
|
||||||
- hhvm
|
- hhvm
|
||||||
|
|
||||||
# run build against nightly but allow them to fail
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- php: nightly
|
|
||||||
- php: hhvm
|
- php: hhvm
|
||||||
|
|
||||||
# faster builds on new travis setup not using sudo
|
# faster builds on new travis setup not using sudo
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
|
# cache vendor dirs
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- vendor
|
||||||
|
- $HOME/.composer/cache
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
|
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
|
||||||
|
# disable TLS for composer because openssl is disabled for PHP 5.3.3 on travis
|
||||||
|
# see: https://blog.travis-ci.com/upcoming_ubuntu_11_10_migration/
|
||||||
|
- if [[ $TRAVIS_PHP_VERSION = 5.3.3 ]]; then composer config -g -- disable-tls true; fi;
|
||||||
|
- if [[ $TRAVIS_PHP_VERSION = 5.3.3 ]]; then composer config -g -- secure-http false; fi;
|
||||||
- composer install --prefer-dist --no-interaction
|
- composer install --prefer-dist --no-interaction
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- phpunit --coverage-clover=coverage.clover
|
- mkdir -p build/logs
|
||||||
|
- phpunit -v --coverage-clover build/logs/clover.xml
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
- |
|
- php vendor/bin/coveralls -v
|
||||||
wget https://scrutinizer-ci.com/ocular.phar
|
|
||||||
php ocular.phar code-coverage:upload --format=php-clover coverage.clover
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Readability
|
# Readability
|
||||||
|
|
||||||
[](https://travis-ci.org/j0k3r/php-readability)
|
[](https://travis-ci.org/j0k3r/php-readability)
|
||||||
[](https://scrutinizer-ci.com/g/j0k3r/php-readability/?branch=master)
|
[](https://coveralls.io/github/j0k3r/php-readability/?branch=master)
|
||||||
|
|
||||||
This is an extract of the Readability class from this [full-text-rss](https://github.com/Dither/full-text-rss) fork. It can be defined as a better version of the original [php-readability](https://bitbucket.org/fivefilters/php-readability/overview).
|
This is an extract of the Readability class from this [full-text-rss](https://github.com/Dither/full-text-rss) fork. It can be defined as a better version of the original [php-readability](https://bitbucket.org/fivefilters/php-readability/overview).
|
||||||
|
|
||||||
@@ -45,3 +45,20 @@ if ($result) {
|
|||||||
echo 'Looks like we couldn\'t find the content. :(';
|
echo 'Looks like we couldn\'t find the content. :(';
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to debug it, or check what's going on, you can inject a logger (which must follow `Psr\Log\LoggerInterface`, Monolog for example):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Readability\Readability;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
|
||||||
|
$url = 'http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/729-thatcher.html';
|
||||||
|
$html = file_get_contents($url);
|
||||||
|
|
||||||
|
$logger = new Logger('readability');
|
||||||
|
$logger->pushHandler(new StreamHandler('path/to/your.log', Logger::DEBUG));
|
||||||
|
|
||||||
|
$readability = new Readability($html, $url);
|
||||||
|
$readability->setLogger($logger);
|
||||||
|
```
|
||||||
|
|||||||
+5
-1
@@ -24,7 +24,11 @@
|
|||||||
"role": "Developer (original JS version)"
|
"role": "Developer (original JS version)"
|
||||||
}],
|
}],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.3"
|
"php": ">=5.3.3",
|
||||||
|
"monolog/monolog": "^1.13.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"satooshi/php-coveralls": "~0.6"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": { "Readability\\": "src/" }
|
"psr-4": { "Readability\\": "src/" }
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@
|
|||||||
</whitelist>
|
</whitelist>
|
||||||
</filter>
|
</filter>
|
||||||
|
|
||||||
<logging>
|
<!-- <logging>
|
||||||
<log type="coverage-html" target="coverage" title="Readability" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/>
|
<log type="coverage-html" target="coverage" title="Readability" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/>
|
||||||
</logging>
|
</logging> -->
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ class JSLikeHTMLElement extends \DOMElement
|
|||||||
$f = $this->ownerDocument->createDocumentFragment();
|
$f = $this->ownerDocument->createDocumentFragment();
|
||||||
|
|
||||||
// appendXML() expects well-formed markup (XHTML)
|
// appendXML() expects well-formed markup (XHTML)
|
||||||
$result = @$f->appendXML($value); // @ to suppress PHP warnings
|
// @ to suppress PHP warnings
|
||||||
|
$result = @$f->appendXML($value);
|
||||||
if ($result) {
|
if ($result) {
|
||||||
if ($f->hasChildNodes()) {
|
if ($f->hasChildNodes()) {
|
||||||
$this->appendChild($f);
|
$this->appendChild($f);
|
||||||
@@ -75,6 +76,7 @@ class JSLikeHTMLElement extends \DOMElement
|
|||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
$import = $f->getElementsByTagName('htmlfragment')->item(0);
|
$import = $f->getElementsByTagName('htmlfragment')->item(0);
|
||||||
|
|
||||||
foreach ($import->childNodes as $child) {
|
foreach ($import->childNodes as $child) {
|
||||||
$importedNode = $this->ownerDocument->importNode($child, true);
|
$importedNode = $this->ownerDocument->importNode($child, true);
|
||||||
$this->appendChild($importedNode);
|
$this->appendChild($importedNode);
|
||||||
@@ -102,6 +104,7 @@ class JSLikeHTMLElement extends \DOMElement
|
|||||||
{
|
{
|
||||||
if ($name == 'innerHTML') {
|
if ($name == 'innerHTML') {
|
||||||
$inner = '';
|
$inner = '';
|
||||||
|
|
||||||
foreach ($this->childNodes as $child) {
|
foreach ($this->childNodes as $child) {
|
||||||
$inner .= $this->ownerDocument->saveXML($child);
|
$inner .= $this->ownerDocument->saveXML($child);
|
||||||
}
|
}
|
||||||
|
|||||||
+279
-216
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace Readability;
|
namespace Readability;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerAwareInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Arc90's Readability ported to PHP for FiveFilters.org
|
* Arc90's Readability ported to PHP for FiveFilters.org
|
||||||
* Based on readability.js version 1.7.1 (without multi-page support)
|
* Based on readability.js version 1.7.1 (without multi-page support)
|
||||||
@@ -45,7 +49,7 @@ namespace Readability;
|
|||||||
* existing DOMElement objects without passing an entire HTML document to
|
* existing DOMElement objects without passing an entire HTML document to
|
||||||
* be parsed.
|
* be parsed.
|
||||||
*/
|
*/
|
||||||
class Readability
|
class Readability implements LoggerAwareInterface
|
||||||
{
|
{
|
||||||
public $convertLinksToFootnotes = false;
|
public $convertLinksToFootnotes = false;
|
||||||
public $revertForcedParagraphElements = true;
|
public $revertForcedParagraphElements = true;
|
||||||
@@ -53,23 +57,33 @@ class Readability
|
|||||||
public $articleContent;
|
public $articleContent;
|
||||||
public $original_html;
|
public $original_html;
|
||||||
public $dom;
|
public $dom;
|
||||||
public $url = null; // optional - URL where HTML was retrieved
|
// optional - URL where HTML was retrieved
|
||||||
public $lightClean = true; // preserves more content (experimental)
|
public $url = null;
|
||||||
|
// preserves more content (experimental)
|
||||||
|
public $lightClean = true;
|
||||||
|
// no more used, keept to avoid BC
|
||||||
public $debug = false;
|
public $debug = false;
|
||||||
public $tidied = false;
|
public $tidied = false;
|
||||||
protected $debugText = ''; // error text for one time output
|
// article domain regexp for calibration
|
||||||
protected $domainRegExp = null; // article domain regexp for calibration
|
protected $domainRegExp = null;
|
||||||
protected $body = null; //
|
protected $body = null; //
|
||||||
protected $bodyCache = null; // Cache the body HTML in case we need to re-use it later
|
// Cache the body HTML in case we need to re-use it later
|
||||||
protected $flags = 7; // 1 | 2 | 4; // Start with all processing flags set.
|
protected $bodyCache = null;
|
||||||
protected $success = false; // indicates whether we were able to extract or not
|
// 1 | 2 | 4; // Start with all processing flags set.
|
||||||
|
protected $flags = 7;
|
||||||
|
// indicates whether we were able to extract or not
|
||||||
|
protected $success = false;
|
||||||
|
protected $logger;
|
||||||
|
protected $parser;
|
||||||
|
protected $html;
|
||||||
|
protected $useTidy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All of the regular expressions in use within readability.
|
* All of the regular expressions in use within readability.
|
||||||
* Defined up here so we don't instantiate them repeatedly in loops.
|
* Defined up here so we don't instantiate them repeatedly in loops.
|
||||||
*/
|
*/
|
||||||
public $regexps = array(
|
public $regexps = array(
|
||||||
'unlikelyCandidates' => '/display\s*:\s*none|ignore|\binfo|annoy|clock|date|time|author|intro|links|hidd?e|about|archive|\bprint|bookmark|tags|share|search|social|robot|published|combx|comment|mast(?:head)|subscri|community|category|disqus|extra|head|head(?:er|note)|floor|foot(?:er|note)|menu|tool|function|nav|remark|rss|shoutbox|tool|widget|meta|banner|sponsor|adsense|inner-?ad|ad-|sponsor|\badv\b|\bads\b|agr?egate?|pager|sidebar|popup|tweet|twitter/i',
|
'unlikelyCandidates' => '/display\s*:\s*none|ignore|\binfo|annoy|clock|date|time|author|intro|links|hidd?e|about|archive|\bprint|bookmark|tags|tag-list|share|search|social|robot|published|combx|comment|mast(?:head)|subscri|community|category|disqus|extra|head|head(?:er|note)|floor|foot(?:er|note)|menu|tool|function|nav|remark|rss|shoutbox|tool|widget|meta|banner|sponsor|adsense|inner-?ad|ad-|sponsor|\badv\b|\bads\b|agr?egate?|pager|sidebar|popup|tweet|twitter/i',
|
||||||
'okMaybeItsACandidate' => '/article\b|contain|\bcontent|column|general|detail|shadow|lightbox|blog|body|entry|main|page/i',
|
'okMaybeItsACandidate' => '/article\b|contain|\bcontent|column|general|detail|shadow|lightbox|blog|body|entry|main|page/i',
|
||||||
'positive' => '/read|full|article|body|\bcontent|contain|entry|main|markdown|page|attach|pagination|post|text|blog|story/i',
|
'positive' => '/read|full|article|body|\bcontent|contain|entry|main|markdown|page|attach|pagination|post|text|blog|story/i',
|
||||||
'negative' => '/bottom|stat|info|discuss|e[\-]?mail|comment|reply|log.{2}(n|ed)|sign|single|combx|com-|contact|_nav|link|media|\bout|promo|\bad-|related|scroll|shoutbox|sidebar|sponsor|shopping|teaser|recommend/i',
|
'negative' => '/bottom|stat|info|discuss|e[\-]?mail|comment|reply|log.{2}(n|ed)|sign|single|combx|com-|contact|_nav|link|media|\bout|promo|\bad-|related|scroll|shoutbox|sidebar|sponsor|shopping|teaser|recommend/i',
|
||||||
@@ -105,21 +119,33 @@ class Readability
|
|||||||
);
|
);
|
||||||
// raw HTML filters
|
// raw HTML filters
|
||||||
protected $pre_filters = array(
|
protected $pre_filters = array(
|
||||||
'!<script[^>]*>(.*?)</script>!is' => '', // remove obvious scripts
|
// remove obvious scripts
|
||||||
'!<style[^>]*>(.*?)</style>!is' => '', // remove obvious styles
|
'!<script[^>]*>(.*?)</script>!is' => '',
|
||||||
'!</?span[^>]*>!is' => '', // remove spans as we redefine styles and they're probably special-styled
|
// remove obvious styles
|
||||||
'!<font[^>]*>\s*\[AD\]\s*</font>!is' => '', // HACK: firewall-filtered content
|
'!<style[^>]*>(.*?)</style>!is' => '',
|
||||||
'!(<br[^>]*>[ \r\n\s]*){2,}!i' => '</p><p>', // HACK: replace linebreaks plus br's with p's
|
// remove spans as we redefine styles and they're probably special-styled
|
||||||
//'!</?noscript>!is' => '', // replace noscripts
|
'!</?span[^>]*>!is' => '',
|
||||||
'!<(/?)font[^>]*>!is' => '<\\1span>', // replace fonts to spans
|
// HACK: firewall-filtered content
|
||||||
|
'!<font[^>]*>\s*\[AD\]\s*</font>!is' => '',
|
||||||
|
// HACK: replace linebreaks plus br's with p's
|
||||||
|
'!(<br[^>]*>[ \r\n\s]*){2,}!i' => '</p><p>',
|
||||||
|
// replace noscripts
|
||||||
|
//'!</?noscript>!is' => '',
|
||||||
|
// replace fonts to spans
|
||||||
|
'!<(/?)font[^>]*>!is' => '<\\1span>',
|
||||||
);
|
);
|
||||||
// output HTML filters
|
// output HTML filters
|
||||||
protected $post_filters = array(
|
protected $post_filters = array(
|
||||||
'/<br\s*\/?>\s*<p/i' => '<p', // replace excessive br's
|
// replace excessive br's
|
||||||
'!<(?:a|div|p)[^>]+/>!is' => '', // replace empty tags that break layouts
|
'/<br\s*\/?>\s*<p/i' => '<p',
|
||||||
//'!<(\s*/?\s*(?:blockquote|br|hr|code|div|article|span|footer|aside|p|pre|dl|li|ul|ol)) [^>]+>!is' => "<\\1>", // remove all attributes on text tags
|
// replace empty tags that break layouts
|
||||||
"/\n+/" => "\n", //single newlines cleanup
|
'!<(?:a|div|p)[^>]+/>!is' => '',
|
||||||
'!<pre[^>]*>\s*<code!is' => '<pre', // modern web...
|
// remove all attributes on text tags
|
||||||
|
//'!<(\s*/?\s*(?:blockquote|br|hr|code|div|article|span|footer|aside|p|pre|dl|li|ul|ol)) [^>]+>!is' => "<\\1>",
|
||||||
|
//single newlines cleanup
|
||||||
|
"/\n+/" => "\n",
|
||||||
|
// modern web...
|
||||||
|
'!<pre[^>]*>\s*<code!is' => '<pre',
|
||||||
'!</code>\s*</pre>!is' => '</pre>',
|
'!</code>\s*</pre>!is' => '</pre>',
|
||||||
'!<[hb]r>!is' => '<\\1 />',
|
'!<[hb]r>!is' => '<\\1 />',
|
||||||
);
|
);
|
||||||
@@ -151,70 +177,23 @@ class Readability
|
|||||||
public function __construct($html, $url = null, $parser = 'libxml', $use_tidy = true)
|
public function __construct($html, $url = null, $parser = 'libxml', $use_tidy = true)
|
||||||
{
|
{
|
||||||
$this->url = $url;
|
$this->url = $url;
|
||||||
$this->debugText = 'Parsing URL: '.$url."\n";
|
$this->html = $html;
|
||||||
|
$this->parser = $parser;
|
||||||
|
$this->useTidy = $use_tidy && function_exists('tidy_parse_string');
|
||||||
|
|
||||||
if ($url) {
|
$this->logger = new NullLogger();
|
||||||
$this->domainRegExp = '/'.strtr(preg_replace('/www\d*\./', '', parse_url($url, PHP_URL_HOST)), array('.' => '\.')).'/';
|
$this->loadHtml();
|
||||||
}
|
}
|
||||||
|
|
||||||
mb_internal_encoding('UTF-8');
|
public function setLogger(LoggerInterface $logger)
|
||||||
mb_http_output('UTF-8');
|
{
|
||||||
mb_regex_encoding('UTF-8');
|
$this->logger = $logger;
|
||||||
|
|
||||||
// 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) {
|
|
||||||
$html = preg_replace($search, $replace, $html);
|
|
||||||
}
|
|
||||||
unset($search, $replace);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trim($html) === '') {
|
|
||||||
$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 ($use_tidy && function_exists('tidy_parse_string')) {
|
|
||||||
$this->debugText .= 'Tidying document'."\n";
|
|
||||||
$tidy = tidy_parse_string($html, $this->tidy_config, 'UTF8');
|
|
||||||
if (tidy_clean_repair($tidy)) {
|
|
||||||
$this->original_html = $html;
|
|
||||||
$this->tidied = true;
|
|
||||||
$html = $tidy->value;
|
|
||||||
$html = preg_replace('/[\r\n]+/is', "\n", $html);
|
|
||||||
}
|
|
||||||
unset($tidy);
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
|
|
||||||
|
|
||||||
if (!($parser == 'html5lib' && ($this->dom = \HTML5_Parser::parse($html)))) {
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
|
|
||||||
$this->dom = new \DOMDocument();
|
|
||||||
$this->dom->preserveWhiteSpace = false;
|
|
||||||
|
|
||||||
if (PHP_VERSION_ID >= 50400) {
|
|
||||||
$this->dom->loadHTML($html, LIBXML_NOBLANKS | LIBXML_COMPACT | LIBXML_NOERROR);
|
|
||||||
} else {
|
|
||||||
$this->dom->loadHTML($html);
|
|
||||||
}
|
|
||||||
|
|
||||||
libxml_use_internal_errors(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->dom->registerNodeClass('DOMElement', 'Readability\JSLikeHTMLElement');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get article title element.
|
* Get article title element.
|
||||||
*
|
*
|
||||||
* @return DOMElement
|
* @return \DOMElement
|
||||||
*/
|
*/
|
||||||
public function getTitle()
|
public function getTitle()
|
||||||
{
|
{
|
||||||
@@ -224,7 +203,7 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Get article content element.
|
* Get article content element.
|
||||||
*
|
*
|
||||||
* @return DOMElement
|
* @return \DOMElement
|
||||||
*/
|
*/
|
||||||
public function getContent()
|
public function getContent()
|
||||||
{
|
{
|
||||||
@@ -253,6 +232,76 @@ class Readability
|
|||||||
$this->post_filters[$filter] = $replacer;
|
$this->post_filters[$filter] = $replacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load HTML in a DOMDocument.
|
||||||
|
* Apply Pre filters
|
||||||
|
* Cleanup HTML using Tidy (or not).
|
||||||
|
*
|
||||||
|
* @todo This should be called in init() instead of from __construct
|
||||||
|
*/
|
||||||
|
private function loadHtml()
|
||||||
|
{
|
||||||
|
$this->original_html = $this->html;
|
||||||
|
|
||||||
|
$this->logger->debug('Parsing URL: '.$this->url);
|
||||||
|
|
||||||
|
if ($this->url) {
|
||||||
|
$this->domainRegExp = '/'.strtr(preg_replace('/www\d*\./', '', parse_url($this->url, PHP_URL_HOST)), array('.' => '\.')).'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
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_parse_string($this->html, $this->tidy_config, 'UTF8');
|
||||||
|
if (tidy_clean_repair($tidy)) {
|
||||||
|
$this->tidied = true;
|
||||||
|
$this->html = $tidy->value;
|
||||||
|
$this->html = preg_replace('/[\r\n]+/is', "\n", $this->html);
|
||||||
|
}
|
||||||
|
unset($tidy);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->html = mb_convert_encoding($this->html, 'HTML-ENTITIES', 'UTF-8');
|
||||||
|
|
||||||
|
if (!($this->parser == 'html5lib' && ($this->dom = \HTML5_Parser::parse($this->html)))) {
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
|
||||||
|
$this->dom = new \DOMDocument();
|
||||||
|
$this->dom->preserveWhiteSpace = false;
|
||||||
|
|
||||||
|
if (PHP_VERSION_ID >= 50400) {
|
||||||
|
$this->dom->loadHTML($this->html, LIBXML_NOBLANKS | LIBXML_COMPACT | LIBXML_NOERROR);
|
||||||
|
} else {
|
||||||
|
$this->dom->loadHTML($this->html);
|
||||||
|
}
|
||||||
|
|
||||||
|
libxml_use_internal_errors(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dom->registerNodeClass('DOMElement', 'Readability\JSLikeHTMLElement');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs readability.
|
* Runs readability.
|
||||||
*
|
*
|
||||||
@@ -283,7 +332,7 @@ class Readability
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($bodyElems->length > 0 && $this->body == null) {
|
if ($bodyElems->length > 0 && $this->body === null) {
|
||||||
$this->body = $bodyElems->item(0);
|
$this->body = $bodyElems->item(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,40 +368,37 @@ class Readability
|
|||||||
// Set title and content instance variables.
|
// Set title and content instance variables.
|
||||||
$this->articleTitle = $articleTitle;
|
$this->articleTitle = $articleTitle;
|
||||||
$this->articleContent = $articleContent;
|
$this->articleContent = $articleContent;
|
||||||
$this->dump_dbg();
|
|
||||||
|
|
||||||
return $this->success;
|
return $this->success;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug.
|
* Debug.
|
||||||
|
*
|
||||||
|
* @deprecated use $this->logger->debug() instead
|
||||||
*/
|
*/
|
||||||
protected function dbg($msg) //, $error=false)
|
protected function dbg($msg)
|
||||||
{
|
{
|
||||||
if ($this->debug) {
|
$this->logger->debug($msg);
|
||||||
$this->debugText .= $msg."\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dump debug info.
|
* Dump debug info.
|
||||||
|
*
|
||||||
|
* @deprecated since Monolog gather log, we don't need it
|
||||||
*/
|
*/
|
||||||
protected function dump_dbg()
|
protected function dump_dbg()
|
||||||
{
|
{
|
||||||
if ($this->debug) {
|
|
||||||
openlog('Readability PHP ', LOG_PID | LOG_PERROR, 0);
|
|
||||||
syslog(6, $this->debugText); // 1 - error 6 - info
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run any post-process modifications to article content as necessary.
|
* Run any post-process modifications to article content as necessary.
|
||||||
*
|
*
|
||||||
* @param DOMElement
|
* @param \DOMElement $articleContent
|
||||||
*/
|
*/
|
||||||
public function postProcessContent($articleContent)
|
public function postProcessContent($articleContent)
|
||||||
{
|
{
|
||||||
if ($this->convertLinksToFootnotes && !preg_match('/\bwiki/', @$this->url)) {
|
if ($this->convertLinksToFootnotes && !preg_match('/\bwiki/', $this->url)) {
|
||||||
$this->addFootnotes($articleContent);
|
$this->addFootnotes($articleContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,16 +406,15 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Get the article title as an H1.
|
* Get the article title as an H1.
|
||||||
*
|
*
|
||||||
* @return DOMElement
|
* @return \DOMElement
|
||||||
*/
|
*/
|
||||||
protected function getArticleTitle()
|
protected function getArticleTitle()
|
||||||
{
|
{
|
||||||
$curTitle = '';
|
|
||||||
$origTitle = '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$curTitle = $origTitle = $this->getInnerText($this->dom->getElementsByTagName('title')->item(0));
|
$curTitle = $origTitle = $this->getInnerText($this->dom->getElementsByTagName('title')->item(0));
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$curTitle = '';
|
||||||
|
$origTitle = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preg_match('/ [\|\-] /', $curTitle)) {
|
if (preg_match('/ [\|\-] /', $curTitle)) {
|
||||||
@@ -410,7 +455,7 @@ class Readability
|
|||||||
* In some cases a body element can't be found (if the HTML is totally hosed for example)
|
* In some cases a body element can't be found (if the HTML is totally hosed for example)
|
||||||
* so we create a new body node and append it to the document.
|
* so we create a new body node and append it to the document.
|
||||||
*/
|
*/
|
||||||
if ($this->body == null) {
|
if ($this->body === null) {
|
||||||
$this->body = $this->dom->createElement('body');
|
$this->body = $this->dom->createElement('body');
|
||||||
$this->dom->documentElement->appendChild($this->body);
|
$this->dom->documentElement->appendChild($this->body);
|
||||||
}
|
}
|
||||||
@@ -433,6 +478,8 @@ class Readability
|
|||||||
* For easier reading, convert this document to have footnotes at the bottom rather than inline links.
|
* For easier reading, convert this document to have footnotes at the bottom rather than inline links.
|
||||||
*
|
*
|
||||||
* @see http://www.roughtype.com/archives/2010/05/experiments_in.php
|
* @see http://www.roughtype.com/archives/2010/05/experiments_in.php
|
||||||
|
*
|
||||||
|
* @param \DOMElement $articleContent
|
||||||
*/
|
*/
|
||||||
public function addFootnotes($articleContent)
|
public function addFootnotes($articleContent)
|
||||||
{
|
{
|
||||||
@@ -496,18 +543,15 @@ class Readability
|
|||||||
* Prepare the article node for display. Clean out any inline styles,
|
* Prepare the article node for display. Clean out any inline styles,
|
||||||
* iframes, forms, strip extraneous <p> tags, etc.
|
* iframes, forms, strip extraneous <p> tags, etc.
|
||||||
*
|
*
|
||||||
* @param DOMElement
|
* @param \DOMElement $articleContent
|
||||||
*/
|
*/
|
||||||
public function prepArticle($articleContent)
|
public function prepArticle($articleContent)
|
||||||
{
|
{
|
||||||
if ($this->lightClean) {
|
$this->logger->debug($this->lightClean ? 'Light clean enabled.' : 'Standard clean enabled.');
|
||||||
$this->dbg('Light clean enabled.');
|
|
||||||
} else {
|
|
||||||
$this->dbg('Standard clean enabled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->cleanStyles($articleContent);
|
$this->cleanStyles($articleContent);
|
||||||
$this->killBreaks($articleContent);
|
$this->killBreaks($articleContent);
|
||||||
|
|
||||||
$xpath = new \DOMXPath($articleContent->ownerDocument);
|
$xpath = new \DOMXPath($articleContent->ownerDocument);
|
||||||
|
|
||||||
if ($this->revertForcedParagraphElements) {
|
if ($this->revertForcedParagraphElements) {
|
||||||
@@ -552,30 +596,31 @@ class Readability
|
|||||||
$this->cleanConditionally($articleContent, 'form');
|
$this->cleanConditionally($articleContent, 'form');
|
||||||
$this->cleanConditionally($articleContent, 'table');
|
$this->cleanConditionally($articleContent, 'table');
|
||||||
$this->cleanConditionally($articleContent, 'ul');
|
$this->cleanConditionally($articleContent, 'ul');
|
||||||
//if (!$this->lightClean)
|
|
||||||
$this->cleanConditionally($articleContent, 'div');
|
$this->cleanConditionally($articleContent, 'div');
|
||||||
|
|
||||||
// Remove extra paragraphs.
|
// Remove extra paragraphs.
|
||||||
$articleParagraphs = $articleContent->getElementsByTagName('p');
|
$articleParagraphs = $articleContent->getElementsByTagName('p');
|
||||||
|
|
||||||
for ($i = $articleParagraphs->length - 1; $i >= 0; --$i) {
|
for ($i = $articleParagraphs->length - 1; $i >= 0; --$i) {
|
||||||
$imgCount = $articleParagraphs->item($i)->getElementsByTagName('img')->length;
|
$item = $articleParagraphs->item($i);
|
||||||
$embedCount = $articleParagraphs->item($i)->getElementsByTagName('embed')->length;
|
|
||||||
$objectCount = $articleParagraphs->item($i)->getElementsByTagName('object')->length;
|
|
||||||
$videoCount = $articleParagraphs->item($i)->getElementsByTagName('video')->length;
|
|
||||||
$audioCount = $articleParagraphs->item($i)->getElementsByTagName('audio')->length;
|
|
||||||
$iframeCount = $articleParagraphs->item($i)->getElementsByTagName('iframe')->length;
|
|
||||||
|
|
||||||
if ($iframeCount === 0 && $imgCount === 0 && $embedCount === 0 && $objectCount === 0 && $videoCount === 0 && $audioCount === 0 && mb_strlen(preg_replace('/\s+/is', '', $this->getInnerText($articleParagraphs->item($i), false, false))) === 0) {
|
$imgCount = $item->getElementsByTagName('img')->length;
|
||||||
$articleParagraphs->item($i)->parentNode->removeChild($articleParagraphs->item($i));
|
$embedCount = $item->getElementsByTagName('embed')->length;
|
||||||
|
$objectCount = $item->getElementsByTagName('object')->length;
|
||||||
|
$videoCount = $item->getElementsByTagName('video')->length;
|
||||||
|
$audioCount = $item->getElementsByTagName('audio')->length;
|
||||||
|
$iframeCount = $item->getElementsByTagName('iframe')->length;
|
||||||
|
|
||||||
|
if ($iframeCount === 0 && $imgCount === 0 && $embedCount === 0 && $objectCount === 0 && $videoCount === 0 && $audioCount === 0 && mb_strlen(preg_replace('/\s+/is', '', $this->getInnerText($item, false, false))) === 0) {
|
||||||
|
$item->parentNode->removeChild($item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add extra text to iframe tag to avoid an auto-closing iframe and then break the html code
|
// add extra text to iframe tag to avoid an auto-closing iframe and then break the html code
|
||||||
if ($iframeCount) {
|
if ($iframeCount) {
|
||||||
$iframe = $articleParagraphs->item($i)->getElementsByTagName('iframe');
|
$iframe = $item->getElementsByTagName('iframe');
|
||||||
$iframe->item(0)->nodeValue = ' ';
|
$iframe->item(0)->nodeValue = ' ';
|
||||||
|
|
||||||
$articleParagraphs->item($i)->parentNode->replaceChild($iframe->item(0), $articleParagraphs->item($i));
|
$item->parentNode->replaceChild($iframe->item(0), $item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,8 +630,8 @@ class Readability
|
|||||||
$articleContent->innerHTML = preg_replace($search, $replace, $articleContent->innerHTML);
|
$articleContent->innerHTML = preg_replace($search, $replace, $articleContent->innerHTML);
|
||||||
}
|
}
|
||||||
unset($search, $replace);
|
unset($search, $replace);
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->dbg('Cleaning output HTML failed. Ignoring: '.$e->getMessage());
|
$this->logger->error('Cleaning output HTML failed. Ignoring: '.$e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -595,7 +640,7 @@ class Readability
|
|||||||
* Initialize a node with the readability object. Also checks the
|
* Initialize a node with the readability object. Also checks the
|
||||||
* className/id for special names to add to its score.
|
* className/id for special names to add to its score.
|
||||||
*
|
*
|
||||||
* @param Element
|
* @param \DOMElement $node
|
||||||
*/
|
*/
|
||||||
protected function initializeNode($node)
|
protected function initializeNode($node)
|
||||||
{
|
{
|
||||||
@@ -622,10 +667,10 @@ class Readability
|
|||||||
case 'FIGURE':
|
case 'FIGURE':
|
||||||
$readability->value += 3;
|
$readability->value += 3;
|
||||||
break;
|
break;
|
||||||
/* case 'SECTION': // often misused
|
case 'SECTION':
|
||||||
$readability->value += 2;
|
// often misused
|
||||||
|
// $readability->value += 2;
|
||||||
break;
|
break;
|
||||||
*/
|
|
||||||
case 'OL':
|
case 'OL':
|
||||||
case 'UL':
|
case 'UL':
|
||||||
case 'DL':
|
case 'DL':
|
||||||
@@ -661,10 +706,12 @@ class Readability
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is
|
* 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.
|
* most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
|
||||||
*
|
*
|
||||||
* @return DOMElement
|
* @param \DOMElement $page
|
||||||
|
*
|
||||||
|
* @return \DOMElement|bool
|
||||||
*/
|
*/
|
||||||
protected function grabArticle($page = null)
|
protected function grabArticle($page = null)
|
||||||
{
|
{
|
||||||
@@ -692,32 +739,30 @@ class Readability
|
|||||||
// (as in, where they contain no other block level elements).
|
// (as in, where they contain no other block level elements).
|
||||||
if (strcasecmp($tagName, 'div') === 0 || strcasecmp($tagName, 'article') === 0 || strcasecmp($tagName, 'section') === 0) {
|
if (strcasecmp($tagName, 'div') === 0 || strcasecmp($tagName, 'article') === 0 || strcasecmp($tagName, 'section') === 0) {
|
||||||
if (!preg_match($this->regexps['divToPElements'], $node->innerHTML)) {
|
if (!preg_match($this->regexps['divToPElements'], $node->innerHTML)) {
|
||||||
//$this->dbg('Altering '.$node->getNodePath().' to p');
|
|
||||||
$newNode = $this->dom->createElement('p');
|
$newNode = $this->dom->createElement('p');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$newNode->innerHTML = $node->innerHTML;
|
$newNode->innerHTML = $node->innerHTML;
|
||||||
// It's easier to debug using original attributes.
|
|
||||||
//$newNode->setAttribute('class', $node->getAttribute('class'));
|
$node->parentNode->replaceChild($newNode, $node);
|
||||||
//$newNode->setAttribute('id', $node->getAttribute('id'));
|
|
||||||
$node = $node->parentNode->replaceChild($newNode, $node);
|
|
||||||
--$nodeIndex;
|
--$nodeIndex;
|
||||||
$nodesToScore[] = $newNode;
|
$nodesToScore[] = $newNode;
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->dbg('Could not alter div/article to p, reverting back to div: '.$e->getMessage());
|
$this->logger->error('Could not alter div/article to p, reverting back to div: '.$e->getMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Will change these P elements back to text nodes after processing.
|
// Will change these P elements back to text nodes after processing.
|
||||||
for ($i = 0, $il = $node->childNodes->length; $i < $il; ++$i) {
|
for ($i = 0, $il = $node->childNodes->length; $i < $il; ++$i) {
|
||||||
$childNode = $node->childNodes->item($i);
|
$childNode = $node->childNodes->item($i);
|
||||||
|
|
||||||
if (is_object($childNode) && get_class($childNode) === 'DOMProcessingInstruction') { //executable tags (<?php or <?xml) warning
|
// executable tags (<?php or <?xml) warning
|
||||||
|
if (is_object($childNode) && get_class($childNode) === 'DOMProcessingInstruction') {
|
||||||
$childNode->parentNode->removeChild($childNode);
|
$childNode->parentNode->removeChild($childNode);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($childNode->nodeType == 3) { // XML_TEXT_NODE
|
if ($childNode->nodeType === XML_TEXT_NODE) {
|
||||||
//$this->dbg('replacing text node with a P tag with the same content.');
|
|
||||||
$p = $this->dom->createElement('p');
|
$p = $this->dom->createElement('p');
|
||||||
$p->innerHTML = $childNode->nodeValue;
|
$p->innerHTML = $childNode->nodeValue;
|
||||||
$p->setAttribute('data-readability-styled', 'true');
|
$p->setAttribute('data-readability-styled', 'true');
|
||||||
@@ -737,12 +782,13 @@ class Readability
|
|||||||
*/
|
*/
|
||||||
for ($pt = 0, $scored = count($nodesToScore); $pt < $scored; ++$pt) {
|
for ($pt = 0, $scored = count($nodesToScore); $pt < $scored; ++$pt) {
|
||||||
$parentNode = $nodesToScore[$pt]->parentNode;
|
$parentNode = $nodesToScore[$pt]->parentNode;
|
||||||
|
|
||||||
// No parent node? Move on...
|
// No parent node? Move on...
|
||||||
if (!$parentNode) {
|
if (!$parentNode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$grandParentNode = ($parentNode->parentNode instanceof DOMElement) ? $parentNode->parentNode : null;
|
$grandParentNode = $parentNode->parentNode instanceof \DOMElement ? $parentNode->parentNode : null;
|
||||||
$innerText = $this->getInnerText($nodesToScore[$pt]);
|
$innerText = $this->getInnerText($nodesToScore[$pt]);
|
||||||
|
|
||||||
// If this paragraph is less than MIN_PARAGRAPH_LENGTH (default:20) characters, don't even count it.
|
// If this paragraph is less than MIN_PARAGRAPH_LENGTH (default:20) characters, don't even count it.
|
||||||
@@ -772,7 +818,7 @@ class Readability
|
|||||||
/* TEST: For every positive/negative parent tag, add/substract half point. Up to 3 points. *\/
|
/* TEST: For every positive/negative parent tag, add/substract half point. Up to 3 points. *\/
|
||||||
$up = $nodesToScore[$pt];
|
$up = $nodesToScore[$pt];
|
||||||
$score = 0;
|
$score = 0;
|
||||||
while ($up->parentNode instanceof DOMElement) {
|
while ($up->parentNode instanceof \DOMElement) {
|
||||||
$up = $up->parentNode;
|
$up = $up->parentNode;
|
||||||
if (preg_match($this->regexps['positive'], $up->getAttribute('class') . ' ' . $up->getAttribute('id'))) {
|
if (preg_match($this->regexps['positive'], $up->getAttribute('class') . ' ' . $up->getAttribute('id'))) {
|
||||||
$score += 0.5;
|
$score += 0.5;
|
||||||
@@ -789,35 +835,38 @@ class Readability
|
|||||||
$grandParentNode->getAttributeNode('readability')->value += $contentScore / self::GRANDPARENT_SCORE_DIVISOR;
|
$grandParentNode->getAttributeNode('readability')->value += $contentScore / self::GRANDPARENT_SCORE_DIVISOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Node prepping: trash nodes that look cruddy (like ones with the class name "comment", etc).
|
* Node prepping: trash nodes that look cruddy (like ones with the class name "comment", etc).
|
||||||
* This is faster to do before scoring but safer after.
|
* This is faster to do before scoring but safer after.
|
||||||
*/
|
*/
|
||||||
if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS) && $xpath) {
|
if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS) && $xpath) {
|
||||||
$candidates = $xpath->query('.//*[(self::footer and count(//footer)<2) or (self::aside and count(//aside)<2)]', $page->documentElement);
|
$candidates = $xpath->query('.//*[(self::footer and count(//footer)<2) or (self::aside and count(//aside)<2)]', $page->documentElement);
|
||||||
|
$node = null;
|
||||||
|
|
||||||
for ($node = null, $c = $candidates->length - 1; $c >= 0; --$c) {
|
for ($c = $candidates->length - 1; $c >= 0; --$c) {
|
||||||
$node = $candidates->item($c);
|
$node = $candidates->item($c);
|
||||||
// node should be readable but not inside of an article otherwise it's probably non-readable block
|
// node should be readable but not inside of an article otherwise it's probably non-readable block
|
||||||
if ($node->hasAttribute('readability') && (int) $node->getAttributeNode('readability')->value < 40 && ($node->parentNode ? strcasecmp($node->parentNode->tagName, 'article') !== 0 : true)) {
|
if ($node->hasAttribute('readability') && (int) $node->getAttributeNode('readability')->value < 40 && ($node->parentNode ? strcasecmp($node->parentNode->tagName, 'article') !== 0 : true)) {
|
||||||
$this->dbg('Removing unlikely candidate '.$node->getNodePath().' by "'.$node->tagName.'" with readability '.($node->hasAttribute('readability') ? (int) $node->getAttributeNode('readability')->value : 0));
|
$this->logger->debug('Removing unlikely candidate (using note) '.$node->getNodePath().' by "'.$node->tagName.'" with readability '.($node->hasAttribute('readability') ? (int) $node->getAttributeNode('readability')->value : 0));
|
||||||
$node->parentNode->removeChild($node);
|
$node->parentNode->removeChild($node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$candidates = $xpath->query('.//*[not(self::body) and (@class or @id or @style) and ((number(@readability) < 40) or not(@readability))]', $page->documentElement);
|
$candidates = $xpath->query('.//*[not(self::body) and (@class or @id or @style) and ((number(@readability) < 40) or not(@readability))]', $page->documentElement);
|
||||||
|
$node = null;
|
||||||
|
|
||||||
for ($node = null, $c = $candidates->length - 1; $c >= 0; --$c) {
|
for ($c = $candidates->length - 1; $c >= 0; --$c) {
|
||||||
$node = $candidates->item($c);
|
$node = $candidates->item($c);
|
||||||
$tagName = $node->tagName;
|
|
||||||
/* Remove unlikely candidates */
|
// Remove unlikely candidates
|
||||||
$unlikelyMatchString = $node->getAttribute('class').' '.$node->getAttribute('id').' '.$node->getAttribute('style');
|
$unlikelyMatchString = $node->getAttribute('class').' '.$node->getAttribute('id').' '.$node->getAttribute('style');
|
||||||
//$this->dbg('Processing '.$node->getNodePath().' by "'. $unlikelyMatchString.'" with readability '.($node->hasAttribute('readability') ? (int)$node->getAttributeNode('readability')->value : 0));
|
|
||||||
if (mb_strlen($unlikelyMatchString) > 3 && // don't process "empty" strings
|
if (mb_strlen($unlikelyMatchString) > 3 && // don't process "empty" strings
|
||||||
preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
|
preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
|
||||||
!preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
|
!preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
|
||||||
) {
|
) {
|
||||||
$this->dbg('Removing unlikely candidate '.$node->getNodePath().' by "'.$unlikelyMatchString.'" with readability '.($node->hasAttribute('readability') ? (int) $node->getAttributeNode('readability')->value : 0));
|
$this->logger->debug('Removing unlikely candidate (using conf) '.$node->getNodePath().' by "'.$unlikelyMatchString.'" with readability '.($node->hasAttribute('readability') ? (int) $node->getAttributeNode('readability')->value : 0));
|
||||||
$node->parentNode->removeChild($node);
|
$node->parentNode->removeChild($node);
|
||||||
--$nodeIndex;
|
--$nodeIndex;
|
||||||
}
|
}
|
||||||
@@ -835,15 +884,17 @@ class Readability
|
|||||||
$candidates = $xpath->query('.//*[@data-candidate]', $page->documentElement);
|
$candidates = $xpath->query('.//*[@data-candidate]', $page->documentElement);
|
||||||
|
|
||||||
for ($c = $candidates->length - 1; $c >= 0; --$c) {
|
for ($c = $candidates->length - 1; $c >= 0; --$c) {
|
||||||
|
$item = $candidates->item($c);
|
||||||
|
|
||||||
// Scale the final candidates score based on link density. Good content should have a
|
// Scale the final candidates score based on link density. Good content should have a
|
||||||
// relatively small link density (5% or less) and be mostly unaffected by this operation.
|
// relatively small link density (5% or less) and be mostly unaffected by this operation.
|
||||||
// If not for this we would have used XPath to find maximum @readability.
|
// If not for this we would have used XPath to find maximum @readability.
|
||||||
$readability = $candidates->item($c)->getAttributeNode('readability');
|
$readability = $item->getAttributeNode('readability');
|
||||||
$readability->value = round($readability->value * (1 - $this->getLinkDensity($candidates->item($c))), 0, PHP_ROUND_HALF_UP);
|
$readability->value = round($readability->value * (1 - $this->getLinkDensity($item)), 0, PHP_ROUND_HALF_UP);
|
||||||
|
|
||||||
if (!$topCandidate || $readability->value > (int) $topCandidate->getAttribute('readability')) {
|
if (!$topCandidate || $readability->value > (int) $topCandidate->getAttribute('readability')) {
|
||||||
$this->dbg('Candidate: '.$candidates->item($c)->getNodePath().' ('.$candidates->item($c)->getAttribute('class').':'.$candidates->item($c)->getAttribute('id').') with score '.$readability->value);
|
$this->logger->debug('Candidate: '.$item->getNodePath().' ('.$item->getAttribute('class').':'.$item->getAttribute('id').') with score '.$readability->value);
|
||||||
$topCandidate = $candidates->item($c);
|
$topCandidate = $item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,9 +911,9 @@ class Readability
|
|||||||
if ($page instanceof \DOMDocument) {
|
if ($page instanceof \DOMDocument) {
|
||||||
if (!isset($page->documentElement)) {
|
if (!isset($page->documentElement)) {
|
||||||
// we don't have a body either? what a mess! :)
|
// we don't have a body either? what a mess! :)
|
||||||
$this->dbg('The page has no body!');
|
$this->logger->debug('The page has no body!');
|
||||||
} else {
|
} else {
|
||||||
$this->dbg('Setting body to a raw HTML of original page!');
|
$this->logger->debug('Setting body to a raw HTML of original page!');
|
||||||
$topCandidate->innerHTML = $page->documentElement->innerHTML;
|
$topCandidate->innerHTML = $page->documentElement->innerHTML;
|
||||||
$page->documentElement->innerHTML = '';
|
$page->documentElement->innerHTML = '';
|
||||||
$this->reinitBody();
|
$this->reinitBody();
|
||||||
@@ -882,7 +933,7 @@ class Readability
|
|||||||
if (strcasecmp($tagName, 'td') === 0 || strcasecmp($tagName, 'tr') === 0) {
|
if (strcasecmp($tagName, 'td') === 0 || strcasecmp($tagName, 'tr') === 0) {
|
||||||
$up = $topCandidate;
|
$up = $topCandidate;
|
||||||
|
|
||||||
if ($up->parentNode instanceof DOMElement) {
|
if ($up->parentNode instanceof \DOMElement) {
|
||||||
$up = $up->parentNode;
|
$up = $up->parentNode;
|
||||||
|
|
||||||
if (strcasecmp($up->tagName, 'table') === 0) {
|
if (strcasecmp($up->tagName, 'table') === 0) {
|
||||||
@@ -891,7 +942,7 @@ class Readability
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dbg('Top candidate: '.$topCandidate->getNodePath());
|
$this->logger->debug('Top candidate: '.$topCandidate->getNodePath());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Now that we have the top candidate, look through its siblings for content that might also be related.
|
* Now that we have the top candidate, look through its siblings for content that might also be related.
|
||||||
@@ -911,9 +962,8 @@ class Readability
|
|||||||
$siblingNode = $siblingNodes->item($s);
|
$siblingNode = $siblingNodes->item($s);
|
||||||
$siblingNodeName = $siblingNode->nodeName;
|
$siblingNodeName = $siblingNode->nodeName;
|
||||||
$append = false;
|
$append = false;
|
||||||
$this->dbg('Looking at sibling node: '.$siblingNode->getNodePath().(($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability')) ? (' with score '.$siblingNode->getAttribute('readability')) : ''));
|
$this->logger->debug('Looking at sibling node: '.$siblingNode->getNodePath().(($siblingNode->nodeType === XML_ELEMENT_NODE && $siblingNode->hasAttribute('readability')) ? (' with score '.$siblingNode->getAttribute('readability')) : ''));
|
||||||
|
|
||||||
//$this->dbg('Sibling has score ' . ($siblingNode->readability ? siblingNode.readability.contentScore : 'Unknown'));
|
|
||||||
if ($siblingNode->isSameNode($topCandidate)) {
|
if ($siblingNode->isSameNode($topCandidate)) {
|
||||||
$append = true;
|
$append = true;
|
||||||
}
|
}
|
||||||
@@ -934,31 +984,25 @@ class Readability
|
|||||||
$nodeContent = $this->getInnerText($siblingNode, true, true);
|
$nodeContent = $this->getInnerText($siblingNode, true, true);
|
||||||
$nodeLength = mb_strlen($nodeContent);
|
$nodeLength = mb_strlen($nodeContent);
|
||||||
|
|
||||||
if ($nodeLength > self::MIN_NODE_LENGTH && $linkDensity < self::MAX_LINK_DENSITY) {
|
if (($nodeLength > self::MIN_NODE_LENGTH && $linkDensity < self::MAX_LINK_DENSITY)
|
||||||
$append = true;
|
|| ($nodeLength < self::MIN_NODE_LENGTH && $linkDensity === 0 && preg_match('/\.( |$)/', $nodeContent))) {
|
||||||
} elseif ($nodeLength < self::MIN_NODE_LENGTH && $linkDensity === 0 && preg_match('/\.( |$)/', $nodeContent)) {
|
|
||||||
$append = true;
|
$append = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($append) {
|
if ($append) {
|
||||||
$this->dbg('Appending node: '.$siblingNode->getNodePath());
|
$this->logger->debug('Appending node: '.$siblingNode->getNodePath());
|
||||||
$nodeToAppend = null;
|
|
||||||
|
|
||||||
if (strcasecmp($siblingNodeName, 'div') !== 0 && strcasecmp($siblingNodeName, 'p') !== 0) {
|
if (strcasecmp($siblingNodeName, 'div') !== 0 && strcasecmp($siblingNodeName, 'p') !== 0) {
|
||||||
/* We have a node that isn't a common block level element, like a form or td tag. Turn it into a div so it doesn't get filtered out later by accident. */
|
// We have a node that isn't a common block level element, like a form or td tag. Turn it into a div so it doesn't get filtered out later by accident.
|
||||||
$this->dbg('Altering siblingNode '.$siblingNodeName.' to div.');
|
$this->logger->debug('Altering siblingNode "'.$siblingNodeName.'" to "div".');
|
||||||
$nodeToAppend = $this->dom->createElement('div');
|
$nodeToAppend = $this->dom->createElement('div');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($siblingNode->getAttribute('id')) {
|
|
||||||
$nodeToAppend->setAttribute('id', $siblingNode->getAttribute('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$nodeToAppend->setAttribute('alt', $siblingNodeName);
|
$nodeToAppend->setAttribute('alt', $siblingNodeName);
|
||||||
$nodeToAppend->innerHTML = $siblingNode->innerHTML;
|
$nodeToAppend->innerHTML = $siblingNode->innerHTML;
|
||||||
} catch (Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->dbg('Could not alter siblingNode '.$siblingNodeName.' to div, reverting to original.');
|
$this->logger->debug('Could not alter siblingNode "'.$siblingNodeName.'" to "div", reverting to original.');
|
||||||
$nodeToAppend = $siblingNode;
|
$nodeToAppend = $siblingNode;
|
||||||
--$s;
|
--$s;
|
||||||
--$sl;
|
--$sl;
|
||||||
@@ -994,17 +1038,17 @@ class Readability
|
|||||||
|
|
||||||
if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS)) {
|
if ($this->flagIsActive(self::FLAG_STRIP_UNLIKELYS)) {
|
||||||
$this->removeFlag(self::FLAG_STRIP_UNLIKELYS);
|
$this->removeFlag(self::FLAG_STRIP_UNLIKELYS);
|
||||||
$this->dbg('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to strip unlikely content.\n");
|
$this->logger->debug('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to strip unlikely content.\n");
|
||||||
|
|
||||||
return $this->grabArticle($this->body);
|
return $this->grabArticle($this->body);
|
||||||
} elseif ($this->flagIsActive(self::FLAG_WEIGHT_ATTRIBUTES)) {
|
} elseif ($this->flagIsActive(self::FLAG_WEIGHT_ATTRIBUTES)) {
|
||||||
$this->removeFlag(self::FLAG_WEIGHT_ATTRIBUTES);
|
$this->removeFlag(self::FLAG_WEIGHT_ATTRIBUTES);
|
||||||
$this->dbg('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to weight attributes.\n");
|
$this->logger->debug('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to weight attributes.\n");
|
||||||
|
|
||||||
return $this->grabArticle($this->body);
|
return $this->grabArticle($this->body);
|
||||||
} elseif ($this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
|
} elseif ($this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
|
||||||
$this->removeFlag(self::FLAG_CLEAN_CONDITIONALLY);
|
$this->removeFlag(self::FLAG_CLEAN_CONDITIONALLY);
|
||||||
$this->dbg('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to clean at all.\n");
|
$this->logger->debug('...content is shorter than '.self::MIN_ARTICLE_LENGTH." letters, trying not to clean at all.\n");
|
||||||
|
|
||||||
return $this->grabArticle($this->body);
|
return $this->grabArticle($this->body);
|
||||||
}
|
}
|
||||||
@@ -1019,7 +1063,7 @@ class Readability
|
|||||||
* Get the inner text of a node.
|
* Get the inner text of a node.
|
||||||
* This also strips out any excess whitespace to be found.
|
* This also strips out any excess whitespace to be found.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
* @param bool $normalizeSpaces (default: true)
|
* @param bool $normalizeSpaces (default: true)
|
||||||
* @param bool $flattenLines (default: false)
|
* @param bool $flattenLines (default: false)
|
||||||
*
|
*
|
||||||
@@ -1027,7 +1071,7 @@ class Readability
|
|||||||
*/
|
*/
|
||||||
public function getInnerText($e, $normalizeSpaces = true, $flattenLines = false)
|
public function getInnerText($e, $normalizeSpaces = true, $flattenLines = false)
|
||||||
{
|
{
|
||||||
if (!isset($e->textContent) || $e->textContent === '') {
|
if (null === $e || !isset($e->textContent) || $e->textContent === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1045,7 +1089,7 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Remove the style attribute on every $e and under.
|
* Remove the style attribute on every $e and under.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
*/
|
*/
|
||||||
public function cleanStyles($e)
|
public function cleanStyles($e)
|
||||||
{
|
{
|
||||||
@@ -1065,7 +1109,7 @@ class Readability
|
|||||||
*
|
*
|
||||||
* @param string $text
|
* @param string $text
|
||||||
*
|
*
|
||||||
* @return number (integer)
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getCommaCount($text)
|
public function getCommaCount($text)
|
||||||
{
|
{
|
||||||
@@ -1078,7 +1122,7 @@ class Readability
|
|||||||
*
|
*
|
||||||
* @param string $text
|
* @param string $text
|
||||||
*
|
*
|
||||||
* @return number (integer)
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getWordCount($text)
|
public function getWordCount($text)
|
||||||
{
|
{
|
||||||
@@ -1090,10 +1134,10 @@ class Readability
|
|||||||
* This is the amount of text that is inside a link divided by the total text in the node.
|
* This is the amount of text that is inside a link divided by the total text in the node.
|
||||||
* Can exclude external references to differentiate between simple text and menus/infoblocks.
|
* Can exclude external references to differentiate between simple text and menus/infoblocks.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
* @param string $excludeExternal
|
* @param string $excludeExternal
|
||||||
*
|
*
|
||||||
* @return number (float)
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getLinkDensity($e, $excludeExternal = false)
|
public function getLinkDensity($e, $excludeExternal = false)
|
||||||
{
|
{
|
||||||
@@ -1119,10 +1163,10 @@ class Readability
|
|||||||
* Get an element weight by attribute.
|
* Get an element weight by attribute.
|
||||||
* Uses regular expressions to tell if this element looks good or bad.
|
* Uses regular expressions to tell if this element looks good or bad.
|
||||||
*
|
*
|
||||||
* @param DOMElement $element
|
* @param \DOMElement $element
|
||||||
* @param string $attribute
|
* @param string $attribute
|
||||||
*
|
*
|
||||||
* @return number (Integer)
|
* @return int
|
||||||
*/
|
*/
|
||||||
protected function weightAttribute($element, $attribute)
|
protected function weightAttribute($element, $attribute)
|
||||||
{
|
{
|
||||||
@@ -1131,19 +1175,20 @@ class Readability
|
|||||||
}
|
}
|
||||||
$weight = 0;
|
$weight = 0;
|
||||||
|
|
||||||
//$attribute_val = trim($element->getAttribute('class')." ".$element->getAttribute('id'));
|
// $attributeValue = trim($element->getAttribute('class')." ".$element->getAttribute('id'));
|
||||||
$attribute_val = trim($element->getAttribute($attribute));
|
$attributeValue = trim($element->getAttribute($attribute));
|
||||||
if ($attribute_val != '') {
|
|
||||||
if (preg_match($this->regexps['negative'], $attribute_val)) {
|
if ($attributeValue != '') {
|
||||||
|
if (preg_match($this->regexps['negative'], $attributeValue)) {
|
||||||
$weight -= 25;
|
$weight -= 25;
|
||||||
}
|
}
|
||||||
if (preg_match($this->regexps['positive'], $attribute_val)) {
|
if (preg_match($this->regexps['positive'], $attributeValue)) {
|
||||||
$weight += 25;
|
$weight += 25;
|
||||||
}
|
}
|
||||||
if (preg_match($this->regexps['unlikelyCandidates'], $attribute_val)) {
|
if (preg_match($this->regexps['unlikelyCandidates'], $attributeValue)) {
|
||||||
$weight -= 5;
|
$weight -= 5;
|
||||||
}
|
}
|
||||||
if (preg_match($this->regexps['okMaybeItsACandidate'], $attribute_val)) {
|
if (preg_match($this->regexps['okMaybeItsACandidate'], $attributeValue)) {
|
||||||
$weight += 5;
|
$weight += 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1154,9 +1199,9 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Get an element relative weight.
|
* Get an element relative weight.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
*
|
*
|
||||||
* @return number (Integer)
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function getWeight($e)
|
public function getWeight($e)
|
||||||
{
|
{
|
||||||
@@ -1165,9 +1210,9 @@ class Readability
|
|||||||
}
|
}
|
||||||
|
|
||||||
$weight = 0;
|
$weight = 0;
|
||||||
/* Look for a special classname */
|
// Look for a special classname
|
||||||
$weight += $this->weightAttribute($e, 'class');
|
$weight += $this->weightAttribute($e, 'class');
|
||||||
/* Look for a special ID */
|
// Look for a special ID
|
||||||
$weight += $this->weightAttribute($e, 'id');
|
$weight += $this->weightAttribute($e, 'id');
|
||||||
|
|
||||||
return $weight;
|
return $weight;
|
||||||
@@ -1176,7 +1221,7 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Remove extraneous break tags from a node.
|
* Remove extraneous break tags from a node.
|
||||||
*
|
*
|
||||||
* @param DOMElement $node
|
* @param \DOMElement $node
|
||||||
*/
|
*/
|
||||||
public function killBreaks($node)
|
public function killBreaks($node)
|
||||||
{
|
{
|
||||||
@@ -1191,33 +1236,34 @@ class Readability
|
|||||||
*
|
*
|
||||||
* Updated 2012-09-18 to preserve youtube/vimeo iframes
|
* Updated 2012-09-18 to preserve youtube/vimeo iframes
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
* @param string $tag
|
* @param string $tag
|
||||||
*/
|
*/
|
||||||
public function clean($e, $tag)
|
public function clean($e, $tag)
|
||||||
{
|
{
|
||||||
|
$currentItem = null;
|
||||||
$targetList = $e->getElementsByTagName($tag);
|
$targetList = $e->getElementsByTagName($tag);
|
||||||
$isEmbed = ($tag === 'audio' || $tag === 'video' || $tag === 'iframe' || $tag === 'object' || $tag === 'embed');
|
$isEmbed = ($tag === 'audio' || $tag === 'video' || $tag === 'iframe' || $tag === 'object' || $tag === 'embed');
|
||||||
|
|
||||||
for ($cur_item = null, $y = $targetList->length - 1; $y >= 0; --$y) {
|
for ($y = $targetList->length - 1; $y >= 0; --$y) {
|
||||||
/* Allow youtube and vimeo videos through as people usually want to see those. */
|
// Allow youtube and vimeo videos through as people usually want to see those.
|
||||||
$cur_item = $targetList->item($y);
|
$currentItem = $targetList->item($y);
|
||||||
|
|
||||||
if ($isEmbed) {
|
if ($isEmbed) {
|
||||||
$attributeValues = $cur_item->getAttribute('src').' '.$cur_item->getAttribute('href');
|
$attributeValues = $currentItem->getAttribute('src').' '.$currentItem->getAttribute('href');
|
||||||
|
|
||||||
/* First, check the elements attributes to see if any of them contain known media hosts */
|
// First, check the elements attributes to see if any of them contain known media hosts
|
||||||
if (preg_match($this->regexps['media'], $attributeValues)) {
|
if (preg_match($this->regexps['media'], $attributeValues)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Then check the elements inside this element for the same. */
|
// Then check the elements inside this element for the same.
|
||||||
if (preg_match($this->regexps['media'], $targetList->item($y)->innerHTML)) {
|
if (preg_match($this->regexps['media'], $targetList->item($y)->innerHTML)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$cur_item->parentNode->removeChild($cur_item);
|
$currentItem->parentNode->removeChild($currentItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,7 +1272,7 @@ class Readability
|
|||||||
* "Fishy" is an algorithm based on content length, classnames,
|
* "Fishy" is an algorithm based on content length, classnames,
|
||||||
* link density, number of images & embeds, etc.
|
* link density, number of images & embeds, etc.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
* @param string $tag
|
* @param string $tag
|
||||||
*/
|
*/
|
||||||
public function cleanConditionally($e, $tag)
|
public function cleanConditionally($e, $tag)
|
||||||
@@ -1237,6 +1283,7 @@ class Readability
|
|||||||
|
|
||||||
$tagsList = $e->getElementsByTagName($tag);
|
$tagsList = $e->getElementsByTagName($tag);
|
||||||
$curTagsLength = $tagsList->length;
|
$curTagsLength = $tagsList->length;
|
||||||
|
$node = null;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Gather counts for other typical elements embedded within.
|
* Gather counts for other typical elements embedded within.
|
||||||
@@ -1244,15 +1291,14 @@ class Readability
|
|||||||
*
|
*
|
||||||
* TODO: Consider taking into account original contentScore here.
|
* TODO: Consider taking into account original contentScore here.
|
||||||
*/
|
*/
|
||||||
for ($node = null, $i = $curTagsLength - 1; $i >= 0; --$i) {
|
for ($i = $curTagsLength - 1; $i >= 0; --$i) {
|
||||||
$node = $tagsList->item($i);
|
$node = $tagsList->item($i);
|
||||||
//$class = $node->getAttribute('class').' '.$node->getAttribute('id'); //debug
|
|
||||||
$weight = $this->getWeight($node);
|
$weight = $this->getWeight($node);
|
||||||
$contentScore = ($node->hasAttribute('readability')) ? (int) $node->getAttribute('readability') : 0;
|
$contentScore = ($node->hasAttribute('readability')) ? (int) $node->getAttribute('readability') : 0;
|
||||||
$this->dbg('Start conditional cleaning of '.$node->getNodePath().' (class='.$node->getAttribute('class').'; id='.$node->getAttribute('id').')'.(($node->hasAttribute('readability')) ? (' with score '.$node->getAttribute('readability')) : ''));
|
$this->logger->debug('Start conditional cleaning of '.$node->getNodePath().' (class='.$node->getAttribute('class').'; id='.$node->getAttribute('id').')'.(($node->hasAttribute('readability')) ? (' with score '.$node->getAttribute('readability')) : ''));
|
||||||
|
|
||||||
if ($weight + $contentScore < 0) {
|
if ($weight + $contentScore < 0) {
|
||||||
$this->dbg('Removing...');
|
$this->logger->debug('Removing...');
|
||||||
$node->parentNode->removeChild($node);
|
$node->parentNode->removeChild($node);
|
||||||
} elseif ($this->getCommaCount($this->getInnerText($node)) < self::MIN_COMMAS_IN_PARAGRAPH) {
|
} elseif ($this->getCommaCount($this->getInnerText($node)) < self::MIN_COMMAS_IN_PARAGRAPH) {
|
||||||
/*
|
/*
|
||||||
@@ -1286,52 +1332,51 @@ class Readability
|
|||||||
|
|
||||||
if ($this->lightClean) {
|
if ($this->lightClean) {
|
||||||
if ($li > $p && $tag != 'ul' && $tag != 'ol') {
|
if ($li > $p && $tag != 'ul' && $tag != 'ol') {
|
||||||
$this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
|
$this->logger->debug(' too many <li> elements, and parent is not <ul> or <ol>');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($input > floor($p / 3)) {
|
} elseif ($input > floor($p / 3)) {
|
||||||
$this->dbg(' too many <input> elements');
|
$this->logger->debug(' too many <input> elements');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($contentLength < 6 && ($embedCount === 0 && ($img === 0 || $img > 2))) {
|
} elseif ($contentLength < 6 && ($embedCount === 0 && ($img === 0 || $img > 2))) {
|
||||||
$this->dbg(' content length less than 6 chars, 0 embeds and either 0 images or more than 2 images');
|
$this->logger->debug(' content length less than 6 chars, 0 embeds and either 0 images or more than 2 images');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($weight < 25 && $linkDensity > 0.25) {
|
} elseif ($weight < 25 && $linkDensity > 0.25) {
|
||||||
$this->dbg(' 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;
|
$toRemove = true;
|
||||||
} elseif ($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
|
} elseif ($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
|
||||||
$this->dbg(' 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;
|
$toRemove = true;
|
||||||
} elseif ($embedCount > 3) {
|
} elseif ($embedCount > 3) {
|
||||||
$this->dbg(' more than 3 embeds');
|
$this->logger->debug(' more than 3 embeds');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($img > $p) {
|
if ($img > $p) {
|
||||||
$this->dbg(' more image elements than paragraph elements');
|
$this->logger->debug(' more image elements than paragraph elements');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($li > $p && $tag != 'ul' && $tag != 'ol') {
|
} elseif ($li > $p && $tag != 'ul' && $tag != 'ol') {
|
||||||
$this->dbg(' too many <li> elements, and parent is not <ul> or <ol>');
|
$this->logger->debug(' too many <li> elements, and parent is not <ul> or <ol>');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($input > floor($p / 3)) {
|
} elseif ($input > floor($p / 3)) {
|
||||||
$this->dbg(' too many <input> elements');
|
$this->logger->debug(' too many <input> elements');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($contentLength < 10 && ($img === 0 || $img > 2)) {
|
} elseif ($contentLength < 10 && ($img === 0 || $img > 2)) {
|
||||||
$this->dbg(' content length less than 10 chars and 0 images, or more than 2 images');
|
$this->logger->debug(' content length less than 10 chars and 0 images, or more than 2 images');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
} elseif ($weight < 25 && $linkDensity > 0.2) {
|
} elseif ($weight < 25 && $linkDensity > 0.2) {
|
||||||
$this->dbg(' 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;
|
$toRemove = true;
|
||||||
} elseif ($weight >= 25 && $linkDensity > 0.5) {
|
} elseif ($weight >= 25 && $linkDensity > 0.5) {
|
||||||
$this->dbg(' 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;
|
$toRemove = true;
|
||||||
} elseif (($embedCount == 1 && $contentLength < 75) || $embedCount > 1) {
|
} elseif (($embedCount == 1 && $contentLength < 75) || $embedCount > 1) {
|
||||||
$this->dbg(' 1 embed and content length smaller than 75 chars, or more than one embed');
|
$this->logger->debug(' 1 embed and content length smaller than 75 chars, or more than one embed');
|
||||||
$toRemove = true;
|
$toRemove = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($toRemove) {
|
if ($toRemove) {
|
||||||
//$this->dbg('Removing: '.$node->innerHTML);
|
$this->logger->debug('Removing...');
|
||||||
$this->dbg('Removing...');
|
|
||||||
$node->parentNode->removeChild($node);
|
$node->parentNode->removeChild($node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1341,12 +1386,13 @@ class Readability
|
|||||||
/**
|
/**
|
||||||
* Clean out spurious headers from an Element. Checks things like classnames and link density.
|
* Clean out spurious headers from an Element. Checks things like classnames and link density.
|
||||||
*
|
*
|
||||||
* @param DOMElement $e
|
* @param \DOMElement $e
|
||||||
*/
|
*/
|
||||||
public function cleanHeaders($e)
|
public function cleanHeaders($e)
|
||||||
{
|
{
|
||||||
for ($headerIndex = 1; $headerIndex < 3; ++$headerIndex) {
|
for ($headerIndex = 1; $headerIndex < 3; ++$headerIndex) {
|
||||||
$headers = $e->getElementsByTagName('h'.$headerIndex);
|
$headers = $e->getElementsByTagName('h'.$headerIndex);
|
||||||
|
|
||||||
for ($i = $headers->length - 1; $i >= 0; --$i) {
|
for ($i = $headers->length - 1; $i >= 0; --$i) {
|
||||||
if ($this->getWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
|
if ($this->getWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
|
||||||
$headers->item($i)->parentNode->removeChild($headers->item($i));
|
$headers->item($i)->parentNode->removeChild($headers->item($i));
|
||||||
@@ -1355,16 +1401,33 @@ class Readability
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given flag is active.
|
||||||
|
*
|
||||||
|
* @param int $flag
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
public function flagIsActive($flag)
|
public function flagIsActive($flag)
|
||||||
{
|
{
|
||||||
return ($this->flags & $flag) > 0;
|
return ($this->flags & $flag) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a flag.
|
||||||
|
*
|
||||||
|
* @param int $flag
|
||||||
|
*/
|
||||||
public function addFlag($flag)
|
public function addFlag($flag)
|
||||||
{
|
{
|
||||||
$this->flags = $this->flags | $flag;
|
$this->flags = $this->flags | $flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a flag.
|
||||||
|
*
|
||||||
|
* @param int $flag
|
||||||
|
*/
|
||||||
public function removeFlag($flag)
|
public function removeFlag($flag)
|
||||||
{
|
{
|
||||||
$this->flags = $this->flags & ~$flag;
|
$this->flags = $this->flags & ~$flag;
|
||||||
|
|||||||
+147
-62
@@ -3,33 +3,33 @@
|
|||||||
namespace Tests\Readability;
|
namespace Tests\Readability;
|
||||||
|
|
||||||
use Readability\Readability;
|
use Readability\Readability;
|
||||||
|
use Monolog\Logger;
|
||||||
class ReadabilityTested extends Readability
|
use Monolog\Handler\TestHandler;
|
||||||
{
|
|
||||||
public function getDebugText()
|
|
||||||
{
|
|
||||||
return $this->debugText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDomainRegexp()
|
|
||||||
{
|
|
||||||
return $this->domainRegExp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
|
public $logHandler;
|
||||||
|
public $logger;
|
||||||
|
|
||||||
|
private function getReadability($html, $url = null, $parser = 'libxml', $useTidy = true)
|
||||||
|
{
|
||||||
|
$readability = new Readability($html, $url, $parser, $useTidy);
|
||||||
|
|
||||||
|
$this->logHandler = new TestHandler();
|
||||||
|
$this->logger = new Logger('test', array($this->logHandler));
|
||||||
|
$readability->setLogger($this->logger);
|
||||||
|
|
||||||
|
return $readability;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @requires extension tidy
|
* @requires extension tidy
|
||||||
*/
|
*/
|
||||||
public function testConstructDefault()
|
public function testConstructDefault()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('');
|
$readability = $this->getReadability('');
|
||||||
|
|
||||||
$this->assertNull($readability->url);
|
$this->assertNull($readability->url);
|
||||||
$this->assertContains('Parsing URL', $readability->getDebugText());
|
|
||||||
$this->assertContains('Tidying document', $readability->getDebugText());
|
|
||||||
$this->assertNull($readability->getDomainRegexp());
|
|
||||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,40 +38,38 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
public function testConstructSimple()
|
public function testConstructSimple()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<html/>', 'http://0.0.0.0');
|
$readability = $this->getReadability('<html/>', 'http://0.0.0.0');
|
||||||
|
|
||||||
$this->assertEquals('http://0.0.0.0', $readability->url);
|
$this->assertEquals('http://0.0.0.0', $readability->url);
|
||||||
$this->assertContains('Parsing URL', $readability->getDebugText());
|
|
||||||
$this->assertContains('Tidying document', $readability->getDebugText());
|
|
||||||
$this->assertEquals('/0\.0\.0\.0/', $readability->getDomainRegexp());
|
|
||||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||||
|
$this->assertEquals('<html/>', $readability->original_html);
|
||||||
|
$this->assertTrue($readability->tidied);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstructDefaultWithoutTidy()
|
public function testConstructDefaultWithoutTidy()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('', null, 'libxml', false);
|
$readability = $this->getReadability('', null, 'libxml', false);
|
||||||
|
|
||||||
$this->assertNull($readability->url);
|
$this->assertNull($readability->url);
|
||||||
$this->assertContains('Parsing URL', $readability->getDebugText());
|
$this->assertEquals('', $readability->original_html);
|
||||||
$this->assertNotContains('Tidying document', $readability->getDebugText());
|
$this->assertFalse($readability->tidied);
|
||||||
$this->assertNull($readability->getDomainRegexp());
|
|
||||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstructSimpleWithoutTidy()
|
public function testConstructSimpleWithoutTidy()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<html/>', 'http://0.0.0.0', 'libxml', false);
|
$readability = $this->getReadability('<html/>', 'http://0.0.0.0', 'libxml', false);
|
||||||
|
|
||||||
$this->assertEquals('http://0.0.0.0', $readability->url);
|
$this->assertEquals('http://0.0.0.0', $readability->url);
|
||||||
$this->assertContains('Parsing URL', $readability->getDebugText());
|
|
||||||
$this->assertNotContains('Tidying document', $readability->getDebugText());
|
|
||||||
$this->assertEquals('/0\.0\.0\.0/', $readability->getDomainRegexp());
|
|
||||||
$this->assertInstanceOf('DomDocument', $readability->dom);
|
$this->assertInstanceOf('DomDocument', $readability->dom);
|
||||||
|
$this->assertEquals('<html/>', $readability->original_html);
|
||||||
|
$this->assertFalse($readability->tidied);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testInitNoContent()
|
public function testInitNoContent()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<html/>', 'http://0.0.0.0');
|
$readability = $this->getReadability('<html/>', 'http://0.0.0.0');
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertFalse($res);
|
$this->assertFalse($res);
|
||||||
@@ -83,7 +81,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testInitP()
|
public function testInitP()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested(str_repeat('<p>This is the awesome content :)</p>', 7), 'http://0.0.0.0');
|
$readability = $this->getReadability(str_repeat('<p>This is the awesome content :)</p>', 7), 'http://0.0.0.0');
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
@@ -96,7 +94,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testInitDivP()
|
public function testInitDivP()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<div>'.str_repeat('<p>This is the awesome content :)</p>', 7).'</div>', 'http://0.0.0.0');
|
$readability = $this->getReadability('<div>'.str_repeat('<p>This is the awesome content :)</p>', 7).'</div>', 'http://0.0.0.0');
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
@@ -109,7 +107,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testInitDiv()
|
public function testInitDiv()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<div>'.str_repeat('This is the awesome content :)', 7).'</div>', 'http://0.0.0.0');
|
$readability = $this->getReadability('<div>'.str_repeat('This is the awesome content :)', 7).'</div>', 'http://0.0.0.0');
|
||||||
$readability->debug = true;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
@@ -123,7 +121,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithFootnotes()
|
public function testWithFootnotes()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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->debug = true;
|
||||||
$readability->convertLinksToFootnotes = true;
|
$readability->convertLinksToFootnotes = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
@@ -140,7 +138,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testStandardClean()
|
public function testStandardClean()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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->debug = true;
|
||||||
$readability->lightClean = false;
|
$readability->lightClean = false;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
@@ -157,7 +155,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithIframe()
|
public function testWithIframe()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
@@ -172,7 +170,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithArticle()
|
public function testWithArticle()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
@@ -187,7 +185,22 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithAside()
|
public function testWithAside()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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);
|
||||||
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
|
$this->assertEmpty($readability->getTitle()->innerHTML);
|
||||||
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
|
$this->assertNotContains('<aside>', $readability->getContent()->innerHTML);
|
||||||
|
$this->assertContains('<footer readability="4"/>', $readability->getContent()->innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithClasses()
|
||||||
|
{
|
||||||
|
$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->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
@@ -197,14 +210,14 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
||||||
$this->assertEmpty($readability->getTitle()->innerHTML);
|
$this->assertEmpty($readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('<aside>', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text should be removed', $readability->getContent()->innerHTML);
|
||||||
$this->assertContains('<footer/>', $readability->getContent()->innerHTML);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWithClasses()
|
public function testWithClassesWithoutLightClean()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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->debug = true;
|
||||||
|
$readability->lightClean = false;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
@@ -218,28 +231,26 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithTd()
|
public function testWithTd()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="tr"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEmpty($readability->getTitle()->innerHTML);
|
$this->assertEmpty($readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testWithSameClasses()
|
public function testWithSameClasses()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEmpty($readability->getTitle()->innerHTML);
|
$this->assertEmpty($readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -247,14 +258,13 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testWithScript()
|
public function testWithScript()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEmpty($readability->getTitle()->innerHTML);
|
$this->assertEmpty($readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -262,14 +272,13 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testTitle()
|
public function testTitle()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEquals('this is my title', $readability->getTitle()->innerHTML);
|
$this->assertEquals('this is my title', $readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -277,14 +286,13 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testTitleWithDash()
|
public function testTitleWithDash()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEquals('title2 - title3', $readability->getTitle()->innerHTML);
|
$this->assertEquals('title2 - title3', $readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -292,14 +300,13 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testTitleWithDoubleDot()
|
public function testTitleWithDoubleDot()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEquals('title2 : title3', $readability->getTitle()->innerHTML);
|
$this->assertEquals('title2 : title3', $readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -307,14 +314,13 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
public function testTitleTooShortUseH1()
|
public function testTitleTooShortUseH1()
|
||||||
{
|
{
|
||||||
$readability = new ReadabilityTested('<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 = $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;
|
$readability->debug = true;
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
|
|
||||||
$this->assertTrue($res);
|
$this->assertTrue($res);
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
$this->assertContains('alt="article"', $readability->getContent()->innerHTML);
|
|
||||||
$this->assertEquals('this is my h1 title !', $readability->getTitle()->innerHTML);
|
$this->assertEquals('this is my h1 title !', $readability->getTitle()->innerHTML);
|
||||||
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
$this->assertContains('This is an awesome text with some links, here there are', $readability->getContent()->innerHTML);
|
||||||
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
$this->assertNotContains('This text is also an awesome text and you should know that', $readability->getContent()->innerHTML);
|
||||||
@@ -322,13 +328,9 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
// public function testConstructParser()
|
// public function testConstructParser()
|
||||||
// {
|
// {
|
||||||
// $readability = new ReadabilityTested('<html/>', 'http://0.0.0.0', 'html5lib');
|
// $readability = $this->getReadability('<html/>', 'http://0.0.0.0', 'html5lib');
|
||||||
|
|
||||||
// $this->assertEquals('http://0.0.0.0', $readability->url);
|
// $this->assertEquals('http://0.0.0.0', $readability->url);
|
||||||
// $this->assertContains('Parsing URL', $readability->getDebugText());
|
|
||||||
// $this->assertContains('Tidying document', $readability->getDebugText());
|
|
||||||
// $this->assertEquals('/0\.0\.0\.0/', $readability->getDomainRegexp());
|
|
||||||
// $this->assertInstanceOf('DomDocument', $readability->dom);
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// dummy function to be used to the next test
|
// dummy function to be used to the next test
|
||||||
@@ -368,7 +370,7 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
</body>
|
</body>
|
||||||
</html>';
|
</html>';
|
||||||
|
|
||||||
$readability = new ReadabilityTested($data, 'http://iosgames.ru/?p=22030');
|
$readability = $this->getReadability($data, 'http://iosgames.ru/?p=22030');
|
||||||
$readability->debug = true;
|
$readability->debug = true;
|
||||||
|
|
||||||
$res = $readability->init();
|
$res = $readability->init();
|
||||||
@@ -379,4 +381,87 @@ class ReadabilityTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertContains('<iframe src="https://www.youtube.com/embed/PUep6xNeKjA" width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen"> </iframe>', $readability->getContent()->innerHTML);
|
$this->assertContains('<iframe src="https://www.youtube.com/embed/PUep6xNeKjA" width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen"> </iframe>', $readability->getContent()->innerHTML);
|
||||||
$this->assertContains('3D Touch', $readability->getTitle()->innerHTML);
|
$this->assertContains('3D Touch', $readability->getTitle()->innerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should generate an Exception "DOMElement::setAttribute(): ID post-60 already defined"
|
||||||
|
*/
|
||||||
|
public function testAppendIdAlreadyHere()
|
||||||
|
{
|
||||||
|
$data = '<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header sml-text-center med-text-left" role="banner">
|
||||||
|
<h1 class="no-margin"><a class="maintitle" href="https://0.0.0.0" title="Bloc-notes">Bloc-notes</a></h1>
|
||||||
|
<h2 class="h5 no-margin"></h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav" role="navigation">
|
||||||
|
<div class="responsive-menu">
|
||||||
|
<label for="menu">Menu</label>
|
||||||
|
<input type="checkbox" id="menu">
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<article class="article" role="article" id="post-60">
|
||||||
|
<section>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are<br/>
|
||||||
|
This is an awesome text with some links, here there are
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
<small>
|
||||||
|
Classé dans : <a class="noactive" title="Services réseaux">Services réseaux</a>
|
||||||
|
</small>
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
|
||||||
|
$readability = $this->getReadability($data, 'http://0.0.0.0');
|
||||||
|
$readability->debug = true;
|
||||||
|
|
||||||
|
$res = $readability->init();
|
||||||
|
|
||||||
|
$this->assertTrue($res);
|
||||||
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getContent());
|
||||||
|
$this->assertInstanceOf('Readability\JSLikeHTMLElement', $readability->getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostFilters()
|
||||||
|
{
|
||||||
|
$readability = $this->getReadability('<div>'.str_repeat('<p>This <b>is</b> the awesome content :)</p>', 7).'</div>', 'http://0.0.0.0');
|
||||||
|
$readability->addPostFilter('!<strong[^>]*>(.*?)</strong>!is', '');
|
||||||
|
|
||||||
|
$res = $readability->init();
|
||||||
|
|
||||||
|
$this->assertTrue($res);
|
||||||
|
$this->assertContains('This the awesome content :)', $readability->getContent()->innerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPreFilters()
|
||||||
|
{
|
||||||
|
$this->markTestSkipped('Won\'t work until loadHtml() is moved in init() instead of __construct()');
|
||||||
|
|
||||||
|
$readability = $this->getReadability('<div>'.str_repeat('<p>This <b>is</b> the awesome and WONDERFUL content :)</p>', 7).'</div>', 'http://0.0.0.0');
|
||||||
|
$readability->addPreFilter('!<b[^>]*>(.*?)</b>!is', '');
|
||||||
|
|
||||||
|
$res = $readability->init();
|
||||||
|
|
||||||
|
$this->assertTrue($res);
|
||||||
|
$this->assertContains('This the awesome and WONDERFUL content :)', $readability->getContent()->innerHTML);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user