update autoloader to consider namespaces for third party libraries: placed and loaded from vendor/namespace/classpath.php

update readability to a newer implementation based on Readability.js (https://github.com/andreskrey/readability.php)
add vendor/Psr/Log interface required for the above
This commit is contained in:
Andrew Dolgov 2018-06-20 14:58:09 +03:00
parent d00d515320
commit 2aaefbfa54
30 changed files with 3494 additions and 19 deletions

View File

@ -2,12 +2,21 @@
require_once "functions.php"; require_once "functions.php";
spl_autoload_register(function($class) { spl_autoload_register(function($class) {
$class_file = str_replace("_", "/", strtolower(basename($class))); list ($namespace, $class_name) = explode('\\', $class, 2);
$file = dirname(__FILE__)."/../classes/$class_file.php"; $root_dir = dirname(__DIR__); // we're in tt-rss/include
if (file_exists($file)) { // 1. third party libraries with namespaces are loaded from vendor/
require $file; // 2. internal tt-rss classes are loaded from classes/ and use special naming logic instead of namespaces
// 3. plugin classes are loaded by PluginHandler from plugins.local/ and plugins/ (TODO: use generic autoloader?)
if ($namespace && $class_name) {
$class_file = "$root_dir/vendor/$namespace/" . str_replace('\\', '/', $class_name) . ".php";
} else {
$class_file = "$root_dir/classes/" . str_replace("_", "/", strtolower($class)) . ".php";
} }
if (file_exists($class_file))
include $class_file;
}); });

View File

@ -1,4 +1,7 @@
<?php <?php
use andreskrey\Readability\Readability;
use andreskrey\Readability\Configuration;
class Af_Readability extends Plugin { class Af_Readability extends Plugin {
/* @var PluginHost $host */ /* @var PluginHost $host */
@ -162,30 +165,35 @@ class Af_Readability extends Plugin {
$tmp = $tmpdoc->saveHTML(); $tmp = $tmpdoc->saveHTML();
} }
$r = new Readability($tmp, $fetch_effective_url); $r = new Readability(new Configuration());
if ($r->init()) { try {
$tmpxpath = new DOMXPath($r->dom); if ($r->parse($tmp)) {
$entries = $tmpxpath->query('(//a[@href]|//img[@src])'); $tmpxpath = new DOMXPath($r->getDOMDOcument());
$entries = $tmpxpath->query('(//a[@href]|//img[@src])');
foreach ($entries as $entry) { foreach ($entries as $entry) {
if ($entry->hasAttribute("href")) { if ($entry->hasAttribute("href")) {
$entry->setAttribute("href", $entry->setAttribute("href",
rewrite_relative_url($fetch_effective_url, $entry->getAttribute("href"))); rewrite_relative_url($fetch_effective_url, $entry->getAttribute("href")));
}
if ($entry->hasAttribute("src")) {
$entry->setAttribute("src",
rewrite_relative_url($fetch_effective_url, $entry->getAttribute("src")));
}
if ($entry->hasAttribute("src")) {
$entry->setAttribute("src",
rewrite_relative_url($fetch_effective_url, $entry->getAttribute("src")));
}
} }
return $r->getContent();
} }
return $r->articleContent->innerHTML; } catch (ParseException $e) {
return false;
} }
} }
return false; return false;

128
vendor/Psr/Log/AbstractLogger.php vendored Normal file
View File

@ -0,0 +1,128 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array())
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array())
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array())
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array())
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array())
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array())
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array())
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array())
{
$this->log(LogLevel::DEBUG, $message, $context);
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Psr\Log;
class InvalidArgumentException extends \InvalidArgumentException
{
}

18
vendor/Psr/Log/LogLevel.php vendored Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace Psr\Log;
/**
* Describes log levels.
*/
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}

18
vendor/Psr/Log/LoggerAwareInterface.php vendored Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace Psr\Log;
/**
* Describes a logger-aware instance.
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object.
*
* @param LoggerInterface $logger
*
* @return void
*/
public function setLogger(LoggerInterface $logger);
}

26
vendor/Psr/Log/LoggerAwareTrait.php vendored Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace Psr\Log;
/**
* Basic Implementation of LoggerAwareInterface.
*/
trait LoggerAwareTrait
{
/**
* The logger instance.
*
* @var LoggerInterface
*/
protected $logger;
/**
* Sets a logger.
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
}

123
vendor/Psr/Log/LoggerInterface.php vendored Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace Psr\Log;
/**
* Describes a logger instance.
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data. The only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array());
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array());
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array());
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array());
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array());
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array());
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array());
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array());
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = array());
}

140
vendor/Psr/Log/LoggerTrait.php vendored Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger trait that classes unable to extend AbstractLogger
* (because they extend another class, etc) can include.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
trait LoggerTrait
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array())
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array())
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array())
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array())
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array())
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array())
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array())
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array())
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
abstract public function log($level, $message, array $context = array());
}

28
vendor/Psr/Log/NullLogger.php vendored Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace Psr\Log;
/**
* This Logger can be used to avoid conditional log calls.
*
* Logging should always be optional, and if no logger is provided to your
* library creating a NullLogger instance to have something to throw logs at
* is a good way to avoid littering your code with `if ($this->logger) { }`
* blocks.
*/
class NullLogger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = array())
{
// noop
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Psr\Log\Test;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* Provides a base test class for ensuring compliance with the LoggerInterface.
*
* Implementors can extend the class and implement abstract methods to run this
* as part of their test suite.
*/
abstract class LoggerInterfaceTest extends \PHPUnit_Framework_TestCase
{
/**
* @return LoggerInterface
*/
abstract public function getLogger();
/**
* This must return the log messages in order.
*
* The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>".
*
* Example ->error('Foo') would yield "error Foo".
*
* @return string[]
*/
abstract public function getLogs();
public function testImplements()
{
$this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
}
/**
* @dataProvider provideLevelsAndMessages
*/
public function testLogsAtAllLevels($level, $message)
{
$logger = $this->getLogger();
$logger->{$level}($message, array('user' => 'Bob'));
$logger->log($level, $message, array('user' => 'Bob'));
$expected = array(
$level.' message of level '.$level.' with context: Bob',
$level.' message of level '.$level.' with context: Bob',
);
$this->assertEquals($expected, $this->getLogs());
}
public function provideLevelsAndMessages()
{
return array(
LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
);
}
/**
* @expectedException \Psr\Log\InvalidArgumentException
*/
public function testThrowsOnInvalidLevel()
{
$logger = $this->getLogger();
$logger->log('invalid level', 'Foo');
}
public function testContextReplacement()
{
$logger = $this->getLogger();
$logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
$expected = array('info {Message {nothing} Bob Bar a}');
$this->assertEquals($expected, $this->getLogs());
}
public function testObjectCastToString()
{
if (method_exists($this, 'createPartialMock')) {
$dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
} else {
$dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
}
$dummy->expects($this->once())
->method('__toString')
->will($this->returnValue('DUMMY'));
$this->getLogger()->warning($dummy);
$expected = array('warning DUMMY');
$this->assertEquals($expected, $this->getLogs());
}
public function testContextCanContainAnything()
{
$context = array(
'bool' => true,
'null' => null,
'string' => 'Foo',
'int' => 0,
'float' => 0.5,
'nested' => array('with object' => new DummyTest),
'object' => new \DateTime,
'resource' => fopen('php://memory', 'r'),
);
$this->getLogger()->warning('Crazy context data', $context);
$expected = array('warning Crazy context data');
$this->assertEquals($expected, $this->getLogs());
}
public function testContextExceptionKeyCanBeExceptionOrOtherValues()
{
$logger = $this->getLogger();
$logger->warning('Random message', array('exception' => 'oops'));
$logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
$expected = array(
'warning Random message',
'critical Uncaught Exception!'
);
$this->assertEquals($expected, $this->getLogs());
}
}
class DummyTest
{
public function __toString()
{
}
}

View File

@ -0,0 +1,348 @@
<?php
namespace andreskrey\Readability;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
/**
* Class Configuration.
*/
class Configuration
{
use LoggerAwareTrait;
/**
* @var int
*/
protected $maxTopCandidates = 5;
/**
* @var int
*/
protected $wordThreshold = 500;
/**
* @var bool
*/
protected $articleByLine = false;
/**
* @var bool
*/
protected $stripUnlikelyCandidates = true;
/**
* @var bool
*/
protected $cleanConditionally = true;
/**
* @var bool
*/
protected $weightClasses = true;
/**
* @var bool
*/
protected $fixRelativeURLs = false;
/**
* @var bool
*/
protected $substituteEntities = false;
/**
* @var bool
*/
protected $normalizeEntities = false;
/**
* @var bool
*/
protected $summonCthulhu = false;
/**
* @var string
*/
protected $originalURL = 'http://fakehost';
/**
* Configuration constructor.
*
* @param array $params
*/
public function __construct(array $params = [])
{
foreach ($params as $key => $value) {
$setter = sprintf('set%s', $key);
if (method_exists($this, $setter)) {
call_user_func([$this, $setter], $value);
}
}
}
/**
* Returns an array-representation of configuration.
*
* @return array
*/
public function toArray()
{
$out = [];
foreach ($this as $key => $value) {
$getter = sprintf('get%s', $key);
if (!is_object($value) && method_exists($this, $getter)) {
$out[$key] = call_user_func([$this, $getter]);
}
}
return $out;
}
/**
* @return LoggerInterface
*/
public function getLogger()
{
// If no logger has been set, just return a null logger
if ($this->logger === null) {
return new NullLogger();
} else {
return $this->logger;
}
}
/**
* @param LoggerInterface $logger
*
* @return Configuration
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
return $this;
}
/**
* @return int
*/
public function getMaxTopCandidates()
{
return $this->maxTopCandidates;
}
/**
* @param int $maxTopCandidates
*
* @return $this
*/
public function setMaxTopCandidates($maxTopCandidates)
{
$this->maxTopCandidates = $maxTopCandidates;
return $this;
}
/**
* @return int
*/
public function getWordThreshold()
{
return $this->wordThreshold;
}
/**
* @param int $wordThreshold
*
* @return $this
*/
public function setWordThreshold($wordThreshold)
{
$this->wordThreshold = $wordThreshold;
return $this;
}
/**
* @return bool
*/
public function getArticleByLine()
{
return $this->articleByLine;
}
/**
* @param bool $articleByLine
*
* @return $this
*/
public function setArticleByLine($articleByLine)
{
$this->articleByLine = $articleByLine;
return $this;
}
/**
* @return bool
*/
public function getStripUnlikelyCandidates()
{
return $this->stripUnlikelyCandidates;
}
/**
* @param bool $stripUnlikelyCandidates
*
* @return $this
*/
public function setStripUnlikelyCandidates($stripUnlikelyCandidates)
{
$this->stripUnlikelyCandidates = $stripUnlikelyCandidates;
return $this;
}
/**
* @return bool
*/
public function getCleanConditionally()
{
return $this->cleanConditionally;
}
/**
* @param bool $cleanConditionally
*
* @return $this
*/
public function setCleanConditionally($cleanConditionally)
{
$this->cleanConditionally = $cleanConditionally;
return $this;
}
/**
* @return bool
*/
public function getWeightClasses()
{
return $this->weightClasses;
}
/**
* @param bool $weightClasses
*
* @return $this
*/
public function setWeightClasses($weightClasses)
{
$this->weightClasses = $weightClasses;
return $this;
}
/**
* @return bool
*/
public function getFixRelativeURLs()
{
return $this->fixRelativeURLs;
}
/**
* @param bool $fixRelativeURLs
*
* @return $this
*/
public function setFixRelativeURLs($fixRelativeURLs)
{
$this->fixRelativeURLs = $fixRelativeURLs;
return $this;
}
/**
* @return bool
*/
public function getSubstituteEntities()
{
return $this->substituteEntities;
}
/**
* @param bool $substituteEntities
*
* @return $this
*/
public function setSubstituteEntities($substituteEntities)
{
$this->substituteEntities = $substituteEntities;
return $this;
}
/**
* @return bool
*/
public function getNormalizeEntities()
{
return $this->normalizeEntities;
}
/**
* @param bool $normalizeEntities
*
* @return $this
*/
public function setNormalizeEntities($normalizeEntities)
{
$this->normalizeEntities = $normalizeEntities;
return $this;
}
/**
* @return string
*/
public function getOriginalURL()
{
return $this->originalURL;
}
/**
* @param string $originalURL
*
* @return $this
*/
public function setOriginalURL($originalURL)
{
$this->originalURL = $originalURL;
return $this;
}
/**
* @return bool
*/
public function getSummonCthulhu()
{
return $this->summonCthulhu;
}
/**
* @param bool $summonCthulhu
*
* @return $this
*/
public function setSummonCthulhu($summonCthulhu)
{
$this->summonCthulhu = $summonCthulhu;
return $this;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMAttr extends \DOMAttr
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMCdataSection extends \DOMCdataSection
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMCharacterData extends \DOMCharacterData
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMComment extends \DOMComment
{
use NodeTrait;
}

View File

@ -0,0 +1,30 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMDocument extends \DOMDocument
{
use NodeTrait;
public function __construct($version, $encoding)
{
parent::__construct($version, $encoding);
$this->registerNodeClass('DOMAttr', DOMAttr::class);
$this->registerNodeClass('DOMCdataSection', DOMCdataSection::class);
$this->registerNodeClass('DOMCharacterData', DOMCharacterData::class);
$this->registerNodeClass('DOMComment', DOMComment::class);
$this->registerNodeClass('DOMDocument', self::class);
$this->registerNodeClass('DOMDocumentFragment', DOMDocumentFragment::class);
$this->registerNodeClass('DOMDocumentType', DOMDocumentType::class);
$this->registerNodeClass('DOMElement', DOMElement::class);
$this->registerNodeClass('DOMEntity', DOMEntity::class);
$this->registerNodeClass('DOMEntityReference', DOMEntityReference::class);
$this->registerNodeClass('DOMNode', DOMNode::class);
$this->registerNodeClass('DOMNotation', DOMNotation::class);
$this->registerNodeClass('DOMProcessingInstruction', DOMProcessingInstruction::class);
$this->registerNodeClass('DOMText', DOMText::class);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMDocumentFragment extends \DOMDocumentFragment
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMDocumentType extends \DOMDocumentType
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMElement extends \DOMElement
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMEntity extends \DOMEntity
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMEntityReference extends \DOMEntityReference
{
use NodeTrait;
}

View File

@ -0,0 +1,13 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
/**
* @method getAttribute($attribute)
*/
class DOMNode extends \DOMNode
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMNotation extends \DOMNotation
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMProcessingInstruction extends \DOMProcessingInstruction
{
use NodeTrait;
}

View File

@ -0,0 +1,10 @@
<?php
namespace andreskrey\Readability\Nodes\DOM;
use andreskrey\Readability\Nodes\NodeTrait;
class DOMText extends \DOMText
{
use NodeTrait;
}

View File

@ -0,0 +1,434 @@
<?php
namespace andreskrey\Readability\Nodes;
use andreskrey\Readability\Nodes\DOM\DOMDocument;
use andreskrey\Readability\Nodes\DOM\DOMElement;
use andreskrey\Readability\Nodes\DOM\DOMNode;
use andreskrey\Readability\Nodes\DOM\DOMText;
/**
* @method \DOMNode removeAttribute($name)
*/
trait NodeTrait
{
/**
* Content score of the node. Used to determine the value of the content.
*
* @var int
*/
public $contentScore = 0;
/**
* Flag for initialized status.
*
* @var bool
*/
private $initialized = false;
/**
* Flag data tables.
*
* @var bool
*/
private $readabilityDataTable = false;
/**
* @var array
*/
private $divToPElements = [
'a',
'blockquote',
'dl',
'div',
'img',
'ol',
'p',
'pre',
'table',
'ul',
'select',
];
/**
* initialized getter.
*
* @return bool
*/
public function isInitialized()
{
return $this->initialized;
}
/**
* @return bool
*/
public function isReadabilityDataTable()
{
return $this->readabilityDataTable;
}
/**
* @param bool $param
*/
public function setReadabilityDataTable($param)
{
$this->readabilityDataTable = $param;
}
/**
* Initializer. Calculates the current score of the node and returns a full Readability object.
*
* @ TODO: I don't like the weightClasses param. How can we get the config here?
*
* @param $weightClasses bool Weight classes?
*
* @return static
*/
public function initializeNode($weightClasses)
{
if (!$this->isInitialized()) {
$contentScore = 0;
switch ($this->nodeName) {
case 'div':
$contentScore += 5;
break;
case 'pre':
case 'td':
case 'blockquote':
$contentScore += 3;
break;
case 'address':
case 'ol':
case 'ul':
case 'dl':
case 'dd':
case 'dt':
case 'li':
case 'form':
$contentScore -= 3;
break;
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
case 'th':
$contentScore -= 5;
break;
}
$this->contentScore = $contentScore + ($weightClasses ? $this->getClassWeight() : 0);
$this->initialized = true;
}
return $this;
}
/**
* Override for native getAttribute method. Some nodes have the getAttribute method, some don't, so we need
* to check first the existence of the attributes property.
*
* @param $attributeName string Attribute to retrieve
*
* @return string
*/
public function getAttribute($attributeName)
{
if (!is_null($this->attributes)) {
return parent::getAttribute($attributeName);
}
return '';
}
/**
* Get the ancestors of the current node.
*
* @param int|bool $maxLevel Max amount of ancestors to get. False for all of them
*
* @return array
*/
public function getNodeAncestors($maxLevel = 3)
{
$ancestors = [];
$level = 0;
$node = $this->parentNode;
while ($node && !($node instanceof DOMDocument)) {
$ancestors[] = $node;
$level++;
if ($level === $maxLevel) {
break;
}
$node = $node->parentNode;
}
return $ancestors;
}
/**
* Returns all links from the current element.
*
* @return array
*/
public function getAllLinks()
{
return iterator_to_array($this->getElementsByTagName('a'));
}
/**
* Get the density of links as a percentage of the content
* This is the amount of text that is inside a link divided by the total text in the node.
*
* @return int
*/
public function getLinkDensity()
{
$linkLength = 0;
$textLength = mb_strlen($this->getTextContent(true));
if (!$textLength) {
return 0;
}
$links = $this->getAllLinks();
if ($links) {
/** @var DOMElement $link */
foreach ($links as $link) {
$linkLength += mb_strlen($link->getTextContent(true));
}
}
return $linkLength / $textLength;
}
/**
* Calculates the weight of the class/id of the current element.
*
* @return int
*/
public function getClassWeight()
{
$weight = 0;
// Look for a special classname
$class = $this->getAttribute('class');
if (trim($class)) {
if (preg_match(NodeUtility::$regexps['negative'], $class)) {
$weight -= 25;
}
if (preg_match(NodeUtility::$regexps['positive'], $class)) {
$weight += 25;
}
}
// Look for a special ID
$id = $this->getAttribute('id');
if (trim($id)) {
if (preg_match(NodeUtility::$regexps['negative'], $id)) {
$weight -= 25;
}
if (preg_match(NodeUtility::$regexps['positive'], $id)) {
$weight += 25;
}
}
return $weight;
}
/**
* Returns the full text of the node.
*
* @param bool $normalize Normalize white space?
*
* @return string
*/
public function getTextContent($normalize = false)
{
$nodeValue = $this->nodeValue;
if ($normalize) {
$nodeValue = trim(preg_replace('/\s{2,}/', ' ', $nodeValue));
}
return $nodeValue;
}
/**
* Returns the children of the current node.
*
* @param bool $filterEmptyDOMText Filter empty DOMText nodes?
*
* @return array
*/
public function getChildren($filterEmptyDOMText = false)
{
$ret = iterator_to_array($this->childNodes);
if ($filterEmptyDOMText) {
// Array values is used to discard the key order. Needs to be 0 to whatever without skipping any number
$ret = array_values(array_filter($ret, function ($node) {
return $node->nodeName !== '#text' || mb_strlen(trim($node->nodeValue));
}));
}
return $ret;
}
/**
* Return an array indicating how many rows and columns this table has.
*
* @return array
*/
public function getRowAndColumnCount()
{
$rows = $columns = 0;
$trs = $this->getElementsByTagName('tr');
foreach ($trs as $tr) {
/** @var \DOMElement $tr */
$rowspan = $tr->getAttribute('rowspan');
$rows += ($rowspan || 1);
// Now look for column-related info
$columnsInThisRow = 0;
$cells = $tr->getElementsByTagName('td');
foreach ($cells as $cell) {
/** @var \DOMElement $cell */
$colspan = $cell->getAttribute('colspan');
$columnsInThisRow += ($colspan || 1);
}
$columns = max($columns, $columnsInThisRow);
}
return ['rows' => $rows, 'columns' => $columns];
}
/**
* Creates a new node based on the text content of the original node.
*
* @param $originalNode DOMNode
* @param $tagName string
*
* @return DOMElement
*/
public function createNode($originalNode, $tagName)
{
$text = $originalNode->getTextContent();
$newNode = $originalNode->ownerDocument->createElement($tagName, $text);
return $newNode;
}
/**
* Check if a given node has one of its ancestor tag name matching the
* provided one.
*
* @param DOMElement $node
* @param string $tagName
* @param int $maxDepth
*
* @return bool
*/
public function hasAncestorTag($node, $tagName, $maxDepth = 3)
{
$depth = 0;
while ($node->parentNode) {
if ($maxDepth > 0 && $depth > $maxDepth) {
return false;
}
if ($node->parentNode->nodeName === $tagName) {
return true;
}
$node = $node->parentNode;
$depth++;
}
return false;
}
/**
* Checks if the current node has a single child and if that child is a P node.
* Useful to convert <div><p> nodes to a single <p> node and avoid confusing the scoring system since div with p
* tags are, in practice, paragraphs.
*
* @param DOMNode $node
*
* @return bool
*/
public function hasSinglePNode()
{
// There should be exactly 1 element child which is a P:
if (count($children = $this->getChildren(true)) !== 1 || $children[0]->nodeName !== 'p') {
return false;
}
// And there should be no text nodes with real content (param true on ->getChildren)
foreach ($children as $child) {
/** @var $child DOMNode */
if ($child->nodeType === XML_TEXT_NODE && !preg_match('/\S$/', $child->getTextContent())) {
return false;
}
}
return true;
}
/**
* Check if the current element has a single child block element.
* Block elements are the ones defined in the divToPElements array.
*
* @return bool
*/
public function hasSingleChildBlockElement()
{
$result = false;
if ($this->hasChildNodes()) {
foreach ($this->getChildren() as $child) {
if (in_array($child->nodeName, $this->divToPElements)) {
$result = true;
} else {
// If any of the hasSingleChildBlockElement calls return true, return true then.
/** @var $child DOMElement */
$result = ($result || $child->hasSingleChildBlockElement());
}
}
}
return $result;
}
/**
* Determines if a node has no content or it is just a bunch of dividing lines and/or whitespace.
*
* @return bool
*/
public function isElementWithoutContent()
{
return $this instanceof DOMElement &&
mb_strlen(preg_replace(NodeUtility::$regexps['onlyWhitespace'], '', $this->textContent)) === 0 &&
($this->childNodes->length === 0 ||
$this->childNodes->length === $this->getElementsByTagName('br')->length + $this->getElementsByTagName('hr')->length
/*
* Special PHP DOMDocument case: We also need to count how many DOMText we have inside the node.
* If there's an empty tag with an space inside and a BR (for example "<p> <br/></p>) counting only BRs and
* HRs will will say that the example has 2 nodes, instead of one. This happens because in DOMDocument,
* DOMTexts are also nodes (which doesn't happen in JS). So we need to also count how many DOMText we
* are dealing with (And at this point we know they are empty or are just whitespace, because of the
* mb_strlen in this chain of checks).
*/
+ count(array_filter(iterator_to_array($this->childNodes), function ($child) {
return $child instanceof DOMText;
}))
);
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace andreskrey\Readability\Nodes;
use andreskrey\Readability\Nodes\DOM\DOMDocument;
use andreskrey\Readability\Nodes\DOM\DOMElement;
use andreskrey\Readability\Nodes\DOM\DOMNode;
/**
* Class NodeUtility.
*/
class NodeUtility
{
/**
* Collection of regexps to check the node usability.
*
* @var array
*/
public static $regexps = [
'unlikelyCandidates' => '/banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i',
'okMaybeItsACandidate' => '/and|article|body|column|main|shadow/i',
'extraneous' => '/print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i',
'byline' => '/byline|author|dateline|writtenby|p-author/i',
'replaceFonts' => '/<(\/?)font[^>]*>/gi',
'normalize' => '/\s{2,}/',
'videos' => '/\/\/(www\.)?(dailymotion|youtube|youtube-nocookie|player\.vimeo)\.com/i',
'nextLink' => '/(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i',
'prevLink' => '/(prev|earl|old|new|<|«)/i',
'whitespace' => '/^\s*$/',
'hasContent' => '/\S$/',
'positive' => '/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i',
'negative' => '/hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i',
// \x{00A0} is the unicode version of &nbsp;
'onlyWhitespace' => '/\x{00A0}|\s+/u'
];
/**
* Imported from the Element class on league\html-to-markdown.
*
* @param $node
*
* @return DOMElement
*/
public static function nextElement($node)
{
$next = $node;
while ($next
&& $next->nodeName !== '#text'
&& trim($next->textContent)) {
$next = $next->nextSibling;
}
return $next;
}
/**
* Changes the node tag name. Since tagName on DOMElement is a read only value, this must be done creating a new
* element with the new tag name and importing it to the main DOMDocument.
*
* @param string $value
* @param bool $importAttributes
*
* @return DOMNode
*/
public static function setNodeTag($node, $value, $importAttributes = false)
{
$new = new DOMDocument('1.0', 'utf-8');
$new->appendChild($new->createElement($value));
$children = $node->childNodes;
/** @var $children \DOMNodeList $i */
for ($i = 0; $i < $children->length; $i++) {
$import = $new->importNode($children->item($i), true);
$new->firstChild->appendChild($import);
}
if ($importAttributes) {
// Import attributes from the original node.
foreach ($node->attributes as $attribute) {
$new->firstChild->setAttribute($attribute->nodeName, $attribute->nodeValue);
}
}
// The import must be done on the firstChild of $new, since $new is a DOMDocument and not a DOMElement.
$import = $node->ownerDocument->importNode($new->firstChild, true);
$node->parentNode->replaceChild($import, $node);
return $import;
}
/**
* Removes the current node and returns the next node to be parsed (child, sibling or parent).
*
* @param DOMNode $node
*
* @return DOMNode
*/
public static function removeAndGetNext($node)
{
$nextNode = self::getNextNode($node, true);
$node->parentNode->removeChild($node);
return $nextNode;
}
/**
* Remove the selected node.
*
* @param $node DOMElement
*
* @return void
**/
public static function removeNode($node)
{
$parent = $node->parentNode;
if ($parent) {
$parent->removeChild($node);
}
}
/**
* Returns the next node. First checks for children (if the flag allows it), then for siblings, and finally
* for parents.
*
* @param DOMNode $originalNode
* @param bool $ignoreSelfAndKids
*
* @return DOMNode
*/
public static function getNextNode($originalNode, $ignoreSelfAndKids = false)
{
/*
* Traverse the DOM from node to node, starting at the node passed in.
* Pass true for the second parameter to indicate this node itself
* (and its kids) are going away, and we want the next node over.
*
* Calling this in a loop will traverse the DOM depth-first.
*/
// First check for kids if those aren't being ignored
if (!$ignoreSelfAndKids && $originalNode->firstChild) {
return $originalNode->firstChild;
}
// Then for siblings...
if ($originalNode->nextSibling) {
return $originalNode->nextSibling;
}
// And finally, move up the parent chain *and* find a sibling
// (because this is depth-first traversal, we will have already
// seen the parent nodes themselves).
do {
$originalNode = $originalNode->parentNode;
} while ($originalNode && !$originalNode->nextSibling);
return ($originalNode) ? $originalNode->nextSibling : $originalNode;
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace andreskrey\Readability;
class ParseException extends \Exception
{
}

File diff suppressed because it is too large Load Diff