Compare commits

...
90 Commits
Author SHA1 Message Date
Jérémy BenoistandGitHub 6abe0c4054 Merge pull request #139 from j0k3r/dependabot/composer/rector/rector-2.6.2 2026-08-21 07:56:31 +02:00
Jérémy BenoistandGitHub 00a4d5f34d Merge pull request #138 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.19 2026-08-21 07:56:21 +02:00
dependabot[bot]andGitHub 959c0680a8 Update rector/rector requirement from 2.5.7 to 2.6.2
Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.5.7...2.6.2)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.6.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-21 05:35:43 +00:00
dependabot[bot]andGitHub 74c7b80b32 Update friendsofphp/php-cs-fixer requirement from 3.95.18 to 3.95.19
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.18...v3.95.19)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.19
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-21 05:35:39 +00:00
Jérémy BenoistandGitHub 33b69bdb6b Merge pull request #135 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.18
Update friendsofphp/php-cs-fixer requirement from 3.95.15 to 3.95.18
2026-08-18 09:45:17 +02:00
Jérémy BenoistandGitHub f8aabce4e0 Merge pull request #137 from j0k3r/dependabot/composer/phpstan/phpstan-2.2.8
Update phpstan/phpstan requirement from 2.2.5 to 2.2.8
2026-08-18 09:45:05 +02:00
dependabot[bot]andGitHub 2e9d05c239 Update phpstan/phpstan requirement from 2.2.5 to 2.2.8
Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) to permit the latest version.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-14 05:34:13 +00:00
dependabot[bot]andGitHub 85fbb6cbf3 Update friendsofphp/php-cs-fixer requirement from 3.95.15 to 3.95.18
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.15...v3.95.18)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.18
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-07 05:34:21 +00:00
Jérémy BenoistandGitHub 67a2b07505 Merge pull request #131 from j0k3r/dependabot/composer/rector/rector-2.5.7 2026-07-20 07:20:12 +02:00
Jérémy BenoistandGitHub b71d73f0e0 Merge pull request #132 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.15 2026-07-20 07:20:01 +02:00
dependabot[bot]andGitHub 1dc5f6bc78 Update friendsofphp/php-cs-fixer requirement from 3.95.12 to 3.95.15
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.12...v3.95.15)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.15
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-17 05:34:54 +00:00
dependabot[bot]andGitHub f19d7ba19e Update rector/rector requirement from 2.5.5 to 2.5.7
Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.5.5...2.5.7)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.5.7
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-17 05:34:36 +00:00
Kevin DecherfandGitHub a93488d727 Merge pull request #113 from Kdecherf/fix/loadHtml
Change visibility of Readability::loadHtml() and prevent DOM reset in init()
2026-07-11 21:40:14 +02:00
Kevin Decherf ac24ef54a4 Fix php-cs-fixer
Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2026-07-11 21:37:25 +02:00
Kevin Decherf 76547fef78 Prevent Readability::init() from resetting DOM if already set
Letting init() resetting the DOM may lead to unexpected behaviors in
libraries like Graby.

Related to https://github.com/j0k3r/graby/pull/392

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2026-07-11 21:37:25 +02:00
Kevin Decherf 228bc7ee1d Change visibility of Readability::loadHtml() to public
This change is required for usecases where we want to load the DOM
without going through the whole init() process

Related to https://github.com/j0k3r/graby/pull/392

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2026-07-11 21:37:25 +02:00
Jérémy BenoistandGitHub 35b87585e7 Merge pull request #128 from j0k3r/dependabot/composer/phpstan/phpstan-phpunit-2.0.18
Update phpstan/phpstan-phpunit requirement from 2.0.16 to 2.0.18
2026-07-10 09:45:01 +02:00
dependabot[bot]andGitHub 4230b2d7ca Update phpstan/phpstan-phpunit requirement from 2.0.16 to 2.0.18
Updates the requirements on [phpstan/phpstan-phpunit](https://github.com/phpstan/phpstan-phpunit) to permit the latest version.
- [Release notes](https://github.com/phpstan/phpstan-phpunit/releases)
- [Commits](https://github.com/phpstan/phpstan-phpunit/compare/2.0.16...2.0.18)

---
updated-dependencies:
- dependency-name: phpstan/phpstan-phpunit
  dependency-version: 2.0.18
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 07:26:54 +00:00
Jérémy BenoistandGitHub 85be584f94 Merge pull request #130 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.12
Update friendsofphp/php-cs-fixer requirement from 3.95.11 to 3.95.12
2026-07-10 09:25:25 +02:00
Jérémy BenoistandGitHub 03a960daf0 Merge pull request #129 from j0k3r/dependabot/composer/rector/rector-2.5.5
Update rector/rector requirement from 2.5.2 to 2.5.5
2026-07-10 09:25:14 +02:00
Jérémy BenoistandGitHub 6f62bcf662 Merge pull request #127 from j0k3r/dependabot/composer/phpstan/phpstan-2.2.5
Update phpstan/phpstan requirement from 2.2.3 to 2.2.5
2026-07-10 09:24:55 +02:00
dependabot[bot]andGitHub 3e3114a492 Update friendsofphp/php-cs-fixer requirement from 3.95.11 to 3.95.12
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.11...v3.95.12)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.12
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 05:35:20 +00:00
dependabot[bot]andGitHub f98247ed14 Update rector/rector requirement from 2.5.2 to 2.5.5
Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.5.2...2.5.5)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.5.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 05:34:54 +00:00
dependabot[bot]andGitHub 5a6e525ff5 Update phpstan/phpstan requirement from 2.2.3 to 2.2.5
Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) to permit the latest version.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 05:34:41 +00:00
Jérémy BenoistandGitHub 0de328760a Merge pull request #126 from j0k3r/dependabot/composer/phpstan/phpstan-2.2.3 2026-07-03 07:56:04 +02:00
dependabot[bot]andGitHub 5aa9da6843 Update phpstan/phpstan requirement from 2.2.2 to 2.2.3
Updates the requirements on [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) to permit the latest version.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-03 05:34:19 +00:00
Jérémy BenoistandGitHub 1bb7eec83c Merge pull request #125 from j0k3r/dependabot/composer/rector/rector-2.5.2
Update rector/rector requirement from 2.4.6 to 2.5.2
2026-06-26 08:58:17 +02:00
Jérémy BenoistandGitHub 049bd07074 Merge pull request #124 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.11
Update friendsofphp/php-cs-fixer requirement from 3.95.8 to 3.95.11
2026-06-26 08:58:05 +02:00
dependabot[bot]andGitHub ca1b105f40 Update rector/rector requirement from 2.4.6 to 2.5.2
Updates the requirements on [rector/rector](https://github.com/rectorphp/rector) to permit the latest version.
- [Release notes](https://github.com/rectorphp/rector/releases)
- [Commits](https://github.com/rectorphp/rector/compare/2.4.6...2.5.2)

---
updated-dependencies:
- dependency-name: rector/rector
  dependency-version: 2.5.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-26 05:34:17 +00:00
dependabot[bot]andGitHub 5ad159ebb1 Update friendsofphp/php-cs-fixer requirement from 3.95.8 to 3.95.11
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.8...v3.95.11)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.11
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-26 05:34:09 +00:00
Kevin DecherfandGitHub 1312ffbced Merge pull request #123 from Kdecherf/fix/singletag-type-error
Fix a TypeError when tidy is disabled
2026-06-23 14:12:03 +02:00
Kevin Decherf 266e36c187 Fix a TypeError when tidy is disabled
A logic tries to replace a node with its child node if it is the only
element of the node. However in some cases, especially when tidy is
disabled, children items other than DOMElement may survive (e.g.
DOMComment), leading to a TypeError.

Fixes #122

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2026-06-22 15:51:06 +02:00
Kevin DecherfandGitHub a8b08d8cb2 Merge pull request #114 from Kdecherf/fix/array-filter
Fix "Undefined array key 0" error
2026-06-21 22:46:10 +02:00
Kevin Decherf 7db754debe Fix "Undefined array key 0" error
If a matching element isn't the first child (e.g. preceded by a whitespace
text node), it may sit at a key other than 0 after some cleanup, leading to
a "Undefined array key 0" error.

This fix is now covered by 6 tests.

Fixes #96

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2026-06-21 17:06:38 +02:00
Jérémy BenoistandGitHub 18bfe842f8 Merge pull request #121 from j0k3r/dependabot/composer/friendsofphp/php-cs-fixer-3.95.8
Update friendsofphp/php-cs-fixer requirement from 3.94.2 to 3.95.8
2026-06-19 10:18:22 +02:00
dependabot[bot]andGitHub 63a0db9109 Update friendsofphp/php-cs-fixer requirement from 3.94.2 to 3.95.8
Updates the requirements on [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer) to permit the latest version.
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.94.2...v3.95.8)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 08:08:54 +00:00
Jan Tojnar 63d6ef1a47 .github: Set up Dependabot
This will make GitHub check dependencies in `composer.json` once a week
and open PRs trying to bump each of them whenever a new version is released.
If the CI fails, as is likely the case with PHP-CS-Fixer, we will need
to fix is manually before the PR can be merged.

https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/dependabot-quickstart
2026-06-19 10:05:13 +02:00
Jan Tojnar 116b6c839a JSLikeHTMLElement: Add types to methods
PHP-CS-Fixer 3.95.0 added `void_return` rule to `@Symfony:risky` ruleset.

https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/tag/v3.95.0
https://cs.symfony.com/doc/rules/function_notation/void_return.html

`void` type declaration is available since PHP 7.1:
https://www.php.net/manual/en/language.types.void.php

Magic methods can have type annotations but they must be compatible with the default ones
(not enforced prior to PHP 8.0):
https://php.watch/versions/8.0/magic-method-signatures

Let’s just type all the methods.

This could technically be a BC break if someone is extending
the `JSLikeHTMLElement` class, overriding the methods that previously
did not have return type hint without adding it, and calling
`registerNodeClass` on the public `dom` property but I think it is
pretty unlikely – at least there seem to be no public instances on GitHub.
2026-06-19 10:00:39 +02:00
Jan Tojnar a35f00ebb3 php-cs-fixer: Prevent removing strict_types declaration
PHP-CS-Fixer 3.95.0 started removing the declarations as a part of
`@Symfony:risky` ruleset.

We currently only use it in `rector.php` but it works there so no point
in following Symfony code style on that.

We will probably want to enable `strict_types` globally eventually,
but let’s at least stop removing them for now so that we can bump
PHP-CS-Fixer.

https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases/tag/v3.95.0
https://cs.symfony.com/doc/rules/strict/declare_strict_types.html
2026-06-19 09:51:35 +02:00
Jan Tojnar 0f9f971556 tests: Use class constants instead of strings
Fixed using `composer rector` and then `composer fix`:

https://getrector.com/rule-detail/string-class-name-to-class-constant-rector
2026-06-19 09:50:01 +02:00
Jan Tojnar fd1eaf61a7 composer: Stop using lock file
Since we are a library that supports wide range of PHP versions,
we cannot lock into a single set of versions of run-time dependencies.

https://getcomposer.org/doc/02-libraries.md#lock-file

Composer 1.10.0 introduced `lock` option to prevent generation of `composer.lock` file:

https://github.com/composer/composer/releases/tag/1.10.0
https://getcomposer.org/doc/06-config.md#lock

This will basically make `composer install` work the same way as `composer update`.
2026-06-19 05:36:17 +02:00
Jan Tojnar 3e40d78a67 Pin development dependencies
PHP-CS-Fixer, PHPStan and Rector regularly introduce changes which break CI
in minor versions, forcing us to mix unrelated changes with CI fixes.

Let’s pin the minor version and update it on our own time e.g. with Dependabot.

This is also what PHPStan suggests for projects not using `composer.lock`:
https://phpstan.org/user-guide/backward-compatibility-promise#minimizing-impact-of-type-inference-changes
2026-06-19 05:17:48 +02:00
Jérémy BenoistandGitHub 03533f5e4f Merge pull request #111 from j0k3r/fix/tidy-php85
Fix bad encoding for `tidy_repair_string`
2026-03-27 20:34:19 +01:00
Jeremy Benoist 009b4ab9b8 Add PHP 8.5 on CI 2026-03-27 20:31:52 +01:00
Jeremy Benoist f9e73fb49a Fix bad encoding for tidy_repair_string
Tidy on PHP 8.5 is more restrictive on what can be given as input encoding. Before, it worked as an unknown value was converted to `utf8`.
2026-03-27 20:29:09 +01:00
Jérémy BenoistandGitHub 3042990efc Merge pull request #106 from jtojnar/encode
Fix character decoding regression when `title` precedes `meta[charset]`
2025-06-03 09:22:21 +02:00
Jan Tojnar 8b89d70b1a Fix character decoding regression when title precedes meta[charset]
Because of PHP 8.2 deprecation, in f14428e4c0, we stopped converting non-ASCII characters to HTML entities. Instead, we started to explicitly insert `meta[charset]` tag at the start of the document.

Later, we discovered that was breaking `html[lang]` so, in efbbc86df9, we made the insertion smarter. One of the improvements was that it would not insert the `meta[charset]` tag when it was already present.

That, however, broke websites that had `title` tag before `meta[charset]`. On those, libxml2 would decode the `title` contents as ISO-8859-1.

We could improve the logic (e.g. check that there is not text content before `meta[charset]`) or insert the tag unconditionally but it will probably be simplest to just go back to converting the non-ASCII characters to entities, just using non-deprecated function variant.
2025-05-28 01:10:59 +02:00
Jan Tojnar 3e9b15db46 tests: Check encoding was preserved in testHtmlLang
The fix introduced in efbbc86df9 alongside this test also manipulates `meta[charset]` but we were not checking if it does not break encoding.
2025-05-28 00:51:49 +02:00
Jérémy BenoistandGitHub 7413a38ff0 Merge pull request #104 from jtojnar/html-shadowing
Fix discarding `html[lang]`
2025-03-04 10:20:28 +01:00
Jérémy BenoistandGitHub a18cd0f2a9 Merge pull request #102 from jtojnar/local-no-domain
Do not set domainRegExp for local files
2025-03-04 10:20:15 +01:00
Jan Tojnar efbbc86df9 Fix discarding html[lang]
`DOMDocument::loadHTML` will parse HTML documents as ISO-8859-1 if there is no `meta[charset]` tag. This means that UTF-8-encoded HTML fragments such as those coming from JSON-LD `articleBody` field would be parsed with incorrect encoding.

In f14428e4c0, we tried to resolve it by putting `meta[charset]` tag at the start of the HTML fragment. Unfortunately, it turns out that causes parser to auto-insert a `html` element, losing the attributes of the original `html` tag.

Let’s try to insert the `meta[charset]` tag into the proper place in the HTML document.

We do not need to use the same trick with `JSLikeHTMLElement::__set`.
That expects smaller HTML fragments, not `html` documents, so creating `html` and `head` elements will not be a problem.
2025-03-04 01:51:34 +01:00
Jan Tojnar 541fab34a0 tests: Remove pointless debug assignment
It is unused since 8ab7d76cd5.
2025-03-03 23:55:32 +01:00
Jan Tojnar 90869d877e tests: Use ::class for DOMDocument class name
Also capitalize it properly.
2025-03-03 23:53:33 +01:00
Jan Tojnar c7208f6ad2 Do not set domainRegExp for local files
`parse_url($this->url, \PHP_URL_HOST)` will return `null` for local filesystem path.
Casting it to `string` will produce an empty regular expression,
which would match any link when computing link density.
2025-03-03 23:26:30 +01:00
Jérémy BenoistandGitHub 4258559b8a Merge pull request #100 from jtojnar/phpunit-bridge7
composer: Allow phpunit-bridge 7.0
2025-02-24 09:50:46 +01:00
Jan Tojnar 1ac761d708 composer: Allow phpunit-bridge 7.0 2025-02-24 09:47:14 +01:00
Jérémy BenoistandGitHub d3053fbce4 Merge pull request #99 from jtojnar/phpstan2
phpstan: Upgrade to version 2
2025-02-24 07:37:59 +01:00
Jan Tojnar 4c929754e9 phpstan: Upgrade to version 2
https://github.com/phpstan/phpstan/blob/2.1.x/UPGRADING.md

Required also bumping Rector since it uses PHPStan internally.
2025-02-23 02:49:08 +01:00
Jan Tojnar 1d7cdf3a12 phpstan: Use standard config path
This allows developer to create their own own config file, e.g. for setting `editorUrl`:
https://phpstan.org/user-guide/output-format#opening-file-in-an-editor
2025-02-23 02:46:57 +01:00
Jérémy BenoistandGitHub f825dcf55a Merge pull request #90 from jtojnar/foreaches
Iterate node lists with foreach
2024-10-11 08:56:35 +02:00
Jan Tojnar 9a9373de4b Iterate node lists with foreach
`DOMNodeList` implements `Traversable`.

There are some `for` loops left but we cannot simply replace those:
PHP follows the DOM specification, which requires that `NodeList`
objects in the DOM are live. As a result, any operation that removes
a node list member node from its parent (such as `removeChild`,
`replaceChild` or `appendChild`) will cause the next node
in the iterator to be skipped.

We could work around that by converting those node lists to static arrays
using `iterator_to_array` but not sure if it is worth it.
2024-10-10 09:01:45 +02:00
Jan Tojnar d454c3a462 Remove dead iteration code
This was forgotten in b580cf216d.
2024-10-10 09:01:45 +02:00
Jan Tojnar 8b1ef07401 Extract for-iterated items into variables
This simplifies the code a bit and will make it slightly easier in case we decide to switch to `foreach` iteration.
2024-10-10 09:01:45 +02:00
Jan Tojnar 5885dbbe78 Remove pointless stdClass
`DOMNode::$childNodes` always contained `DOMNodeList`.
2024-10-10 09:01:45 +02:00
Jérémy BenoistandGitHub 6947999782 Merge pull request #92 from jtojnar/ci-fix
ci: Fix & add PHP 8.4
2024-10-10 08:59:28 +02:00
Jan Tojnar da755013aa Remove extra set_error_handler callback argument
It is unused and would cause an error on PHP ≥ 8.0:
https://www.php.net/manual/en/function.set-error-handler.php#refsect1-function.set-error-handler-parameters

Not sure if the handler is even necessary – it was introduced in 175196d6c2 but I did not manage to reproduce the original error (Entity 'nbsp' not defined). It was probably fixed by f2a43b476c.
2024-10-10 08:52:28 +02:00
Jan Tojnar 5b9551d1e3 ci: Add PHP 8.4
PHP 8.4 is in beta, with final version scheduled for November so it is time to start testing it.
2024-10-10 01:27:59 +02:00
Jan Tojnar c7b10dcc45 Avoid E_STRICT constant
It will be deprecated in PHP 8.4 and it is meaningless nowadays anyway:
https://wiki.php.net/rfc/deprecations_php_8_4#remove_e_strict_error_level_and_deprecate_e_strict_constant

The use of the constant was introduced in 175196d6c2.
2024-10-10 01:27:59 +02:00
Jan Tojnar 80adfe870b Fix coding style
With php-cs-fixer 3.64.0, the `native_function_invocation` rule no longer passed.
2024-10-10 01:01:55 +02:00
Jérémy BenoistandGitHub cb6b6ac577 Merge pull request #88 from jtojnar/has-single-fix 2024-03-19 06:02:54 +01:00
Jan Tojnar 677f3f096e Fix hasSingleTagInsideElement method
It would fail for e.g. `<div> <p>foo</p> </div>`.

mozilla/readability uses children for the tag lookup, which return only elements.
PHP does not have children property so b580cf216d
mistakenly used `childNodes` instead, but that can return any node type.

Let’s filter the children ourselves.

Also add comments from mozilla/readability’s `_hasSingleTagInsideElement`.
2024-03-18 23:01:43 +01:00
Jérémy BenoistandGitHub 29122763db Merge pull request #89 from jtojnar/php74
Require PHP 7.4
2024-03-18 09:18:11 +01:00
Jan TojnarandJérémy Benoist 89d3b74259 Rectorize to PHP 7.4
Switches to short anonymous function syntax.
2024-03-18 09:16:43 +01:00
Jan TojnarandJérémy Benoist e792644fe8 Drop PHP < 7.4 support
This will allow us to use flexible heredocs in test,
as well as typed properties and other goodies.

https://www.php.net/releases/7_3_0.php
https://www.php.net/releases/7_4_0.php
2024-03-18 09:16:43 +01:00
Jan TojnarandJérémy Benoist 648d8c605b Update coding style for upcoming PHP-CS-Fixer changes
Once we bump minimum PHP version, we will get newer PHP-CS-Fixer,
which will try to apply this cleanups.

Also manually tweak anonymous functions so that they are cleanly formatted
once we switch to `fn` syntax.
2024-03-18 09:16:43 +01:00
Jérémy BenoistandGitHub f28191a728 Merge pull request #86 from jtojnar/ci-bump
ci: Update actions
2024-03-18 09:12:18 +01:00
Jan Tojnar 2103853a1b ci: Bump coveralls to 2.7.0
- Fixes PHP 8 support https://github.com/php-coveralls/php-coveralls/releases/tag/v2.4.3
2024-03-16 22:30:49 +01:00
Jan Tojnar 7f4c6cfcbd ci: Update actions
Mostly just of nodejs bump:

- https://github.com/actions/checkout/releases/tag/v4.0.0
- https://github.com/ramsey/composer-install/releases/tag/3.0.0
2024-03-16 16:01:16 +01:00
Jérémy BenoistandGitHub 38870cdff1 Merge pull request #80 from jtojnar/stricter
Fix some CI issues
2023-04-03 14:47:32 +02:00
Jan Tojnar 9bdd3b6b2e ci: Add PHP 8.2 and 8.3 2023-03-31 05:26:07 +02:00
Jan Tojnar f14428e4c0 Do not use mb_convert_encoding with HTML-ENTITIES as target encoding
This is deprecated since PHP 8.2:

    Deprecated: mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity instead

It was used because `DOMDocument`, which uses libxml2 internally, will parse the HTML as ISO-8859-1, unless the document contains an XML encoding declaration or HTML meta tag setting character set.
Since first such element wins, putting the `meta[charset]` up front will ensure the parser uses the correct encoding, even if the document contains incorrect meta tag (e.g. when the document is converted to UTF-8 without also updating the metadata by the software passing it to Readability).

https://stackoverflow.com/a/39148511/160386
2023-03-31 05:26:07 +02:00
Jan Tojnar 23f824a1ce tests: Fix “THE ERROR HANDLER HAS CHANGED!” 2023-03-31 03:19:22 +02:00
Jan Tojnar 2a57124528 composer: upgrade rector 2023-03-31 03:19:22 +02:00
Jan Tojnar 0975574bdb Rector: Upgrade configuration 2023-03-31 03:19:22 +02:00
Jan Tojnar 9ed89bde92 Fix PHP-Cs-Fixer changes
1) src/Readability.php (braces, no_unneeded_control_parentheses, single_line_comment_spacing, global_namespace_import, no_unused_imports, phpdoc_align)
   2) src/JSLikeHTMLElement.php (phpdoc_separation)

Switch code blocks to Markdown syntax to work around `phpdoc_separation`, ApiGen uses Markdown these days anyway.
2023-03-31 03:14:00 +02:00
Jan Tojnar 2c6c6d5987 PHPStan: Use stable PHPUnit path
phpunit-bridge will create a symlink.
2023-03-31 03:14:00 +02:00
Jan Tojnar c5407ec07c composer: Add scripts for development 2023-03-31 03:14:00 +02:00
Jérémy BenoistandGitHub 7cd8476d38 Merge pull request #79 from j0k3r/fix/psr-log-2-3
Allow `psr/log` 2.0 & 3.0
2022-10-17 22:44:36 +02:00
Jeremy Benoist 82083c872b Allow psr/log 2.0 & 3.0 2022-10-17 22:42:47 +02:00
Kevin DecherfandJeremy Benoist 6689f19956 Strip script and style tags through ::clean() method instead of preg_replace
Huge tags can lead to a failure of preg_replace, thus erasing the whole
fetched content.

Fixes https://github.com/wallabag/wallabag/issues/5847

Signed-off-by: Kevin Decherf <kevin@kdecherf.com>
2022-06-13 09:13:23 +02:00
11 changed files with 435 additions and 287 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
+3 -3
View File
@@ -11,7 +11,7 @@ on:
jobs:
coding-standards:
name: "CS Fixer & PHPStan"
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
strategy:
matrix:
@@ -20,7 +20,7 @@ jobs:
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
uses: "actions/checkout@v4"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
@@ -33,7 +33,7 @@ jobs:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--optimize-autoloader --prefer-dist"
+16 -14
View File
@@ -14,20 +14,22 @@ env:
jobs:
phpunit:
name: "PHPUnit (PHP ${{ matrix.php }})"
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
strategy:
matrix:
php:
- "7.2"
- "7.3"
- "7.4"
- "8.0"
- "8.1"
- "8.2"
- "8.3"
- "8.4"
- "8.5"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -46,7 +48,7 @@ jobs:
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--optimize-autoloader --prefer-dist"
@@ -58,16 +60,16 @@ jobs:
phpunit-coverage:
name: "PHPUnit coverage (PHP ${{ matrix.php }})"
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
strategy:
matrix:
php:
- "7.4"
- "8.0"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -86,7 +88,7 @@ jobs:
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--optimize-autoloader --prefer-dist"
@@ -97,7 +99,7 @@ jobs:
run: "php vendor/bin/simple-phpunit -v --coverage-clover build/logs/clover.xml"
- name: "Retrieve Coveralls phar"
run: "wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.2/php-coveralls.phar"
run: "wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.7.0/php-coveralls.phar"
- name: "Enable Coveralls phar"
run: "chmod +x php-coveralls.phar"
@@ -109,16 +111,16 @@ jobs:
phpunit-lowest:
name: "PHPUnit lowest deps (PHP ${{ matrix.php }})"
runs-on: "ubuntu-20.04"
runs-on: "ubuntu-22.04"
strategy:
matrix:
php:
- "7.2"
- "7.4"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
uses: "actions/checkout@v4"
with:
fetch-depth: 2
@@ -137,7 +139,7 @@ jobs:
run: "composer remove friendsofphp/php-cs-fixer --dev --no-progress --no-update"
- name: "Install dependencies with Composer"
uses: "ramsey/composer-install@v1"
uses: "ramsey/composer-install@v3"
with:
composer-options: "--optimize-autoloader --prefer-dist"
dependency-versions: "lowest"
+1
View File
@@ -4,3 +4,4 @@ composer.lock
.php_cs.cache
.php-cs-fixer.cache
.phpunit.result.cache
phpstan.neon
+6
View File
@@ -26,6 +26,12 @@ return (new PhpCsFixer\Config())
'strict_comparison' => true,
'strict_param' => true,
'concat_space' => ['spacing' => 'one'],
// Pulled in by @Symfony:risky but we still support PHP 7.4
'modernize_strpos' => false,
// Pulled in by @Symfony, we cannot add property types until we bump PHP to ≥ 7.4
'no_null_property_initialization' => false,
// @Symfony:risky started removing them but we will probably want to go the opposite direction.
'declare_strict_types' => false,
])
->setFinder($finder)
;
+14 -10
View File
@@ -24,18 +24,18 @@
"role": "Developer (original JS version)"
}],
"require": {
"php": ">=7.2.0",
"php": ">=7.4.0",
"ext-mbstring": "*",
"psr/log": "^1.0",
"psr/log": "^1.0.1 || ^2.0 || ^3.0",
"masterminds/html5": "^2.7"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"friendsofphp/php-cs-fixer": "3.95.19",
"monolog/monolog": "^1.24|^2.1",
"symfony/phpunit-bridge": "^4.4|^5.3|^6.0",
"phpstan/phpstan": "^1.3",
"phpstan/phpstan-phpunit": "^1.0",
"rector/rector": "^0.12.15"
"symfony/phpunit-bridge": "^4.4|^5.3|^6.0|^7.0",
"phpstan/phpstan": "2.2.8",
"phpstan/phpstan-phpunit": "2.0.18",
"rector/rector": "2.6.2"
},
"suggest": {
"ext-tidy": "Used to clean up given HTML and to avoid problems with bad HTML structure."
@@ -47,8 +47,12 @@
"psr-4": { "Tests\\Readability\\": "tests/" }
},
"config": {
"platform": {
"php": "7.2.34"
}
"lock": false
},
"scripts": {
"fix": "php-cs-fixer fix --verbose --diff",
"phpstan": "phpstan analyze --memory-limit 512M",
"rector": "rector process",
"test": "simple-phpunit -v"
}
}
+1 -3
View File
@@ -6,9 +6,7 @@ parameters:
# https://github.com/phpstan/phpstan/issues/694#issuecomment-350724288
bootstrapFiles:
- vendor/bin/.phpunit/phpunit-8.5-0/vendor/autoload.php
checkMissingIterableValueType: false
- vendor/bin/.phpunit/phpunit/vendor/autoload.php
includes:
- vendor/phpstan/phpstan-phpunit/extension.neon
+9 -23
View File
@@ -2,31 +2,17 @@
declare(strict_types=1);
use Rector\Core\Configuration\Option;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
// paths to refactor; solid alternative to CLI arguments
$parameters->set(Option::PATHS, [
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
]);
// Path to phpstan with extensions, that PHPSTan in Rector uses to determine types
$parameters->set(Option::PHPSTAN_FOR_RECTOR_PATH, __DIR__ . '/phpstan.neon');
$parameters->set(Option::BOOTSTRAP_FILES, [
__DIR__ . '/vendor/bin/.phpunit/phpunit-8.5-0/vendor/autoload.php',
])
->withBootstrapFiles([
__DIR__ . '/vendor/bin/.phpunit/phpunit/vendor/autoload.php',
__DIR__ . '/vendor/autoload.php',
]);
// Define what rule sets will be applied
$containerConfigurator->import(LevelSetList::UP_TO_PHP_72);
// is your PHP version different from the one your refactor to?
$parameters->set(Option::PHP_VERSION_FEATURES, PhpVersion::PHP_72);
};
])
->withSets([LevelSetList::UP_TO_PHP_74])
;
+13 -12
View File
@@ -39,11 +39,11 @@ class JSLikeHTMLElement extends \DOMElement
/**
* Used for setting innerHTML like it's done in JavaScript:.
*
* @code
* ```php
* $div->innerHTML = '<h2>Chapter 2</h2><p>The story begins...</p>';
* @endcode
* ```
*/
public function __set($name, $value)
public function __set(string $name, string $value): void
{
if ('innerHTML' !== $name) {
$trace = debug_backtrace();
@@ -79,14 +79,13 @@ class JSLikeHTMLElement extends \DOMElement
} else {
// $value is probably ill-formed
$f = new \DOMDocument();
$value = mb_convert_encoding($value, 'HTML-ENTITIES', 'UTF-8');
// Using <htmlfragment> will generate a warning, but so will bad HTML
// (and by this point, bad HTML is what we've got).
// We use it (and suppress the warning) because an HTML fragment will
// be wrapped around <html><body> tags which we don't really want to keep.
// Note: despite the warning, if loadHTML succeeds it will return true.
$result = $f->loadHTML('<htmlfragment>' . $value . '</htmlfragment>');
$result = $f->loadHTML('<meta charset="utf-8"><htmlfragment>' . $value . '</htmlfragment>');
if ($result) {
$import = $f->getElementsByTagName('htmlfragment')->item(0);
@@ -105,11 +104,11 @@ class JSLikeHTMLElement extends \DOMElement
/**
* Used for getting innerHTML like it's done in JavaScript:.
*
* @code
* ```php
* $string = $div->innerHTML;
* @endcode
* ```
*/
public function __get($name)
public function __get(string $name): string
{
if ('innerHTML' === $name) {
$inner = '';
@@ -125,20 +124,22 @@ class JSLikeHTMLElement extends \DOMElement
$trace = debug_backtrace();
trigger_error('Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], \E_USER_NOTICE);
return '';
}
public function __toString()
public function __toString(): string
{
return '[' . $this->tagName . ']';
}
public function getInnerHtml()
public function getInnerHtml(): string
{
return $this->__get('innerHTML');
}
public function setInnerHtml($value)
public function setInnerHtml(string $value): void
{
return $this->__set('innerHTML', $value);
$this->__set('innerHTML', $value);
}
}
+193 -151
View File
@@ -2,7 +2,6 @@
namespace Readability;
use DOMElement;
use Masterminds\HTML5;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
@@ -89,7 +88,7 @@ class Readability implements LoggerAwareInterface
'enclose-text' => true,
'merge-divs' => true,
// 'merge-spans' => true,
'input-encoding' => '????',
'input-encoding' => 'utf8',
'output-encoding' => 'utf8',
'hide-comments' => true,
];
@@ -108,10 +107,6 @@ class Readability implements LoggerAwareInterface
protected $useTidy;
// raw HTML filters
protected $pre_filters = [
// remove obvious scripts
'!<script[^>]*>(.*?)</script>!is' => '',
// remove obvious styles
'!<style[^>]*>(.*?)</style>!is' => '',
// remove spans as we redefine styles and they're probably special-styled
'!</?span[^>]*>!is' => '',
// HACK: firewall-filtered content
@@ -147,7 +142,7 @@ class Readability implements LoggerAwareInterface
* @param string $parser Which parser to use for turning raw HTML into a DOMDocument
* @param bool $useTidy Use tidy
*/
public function __construct(string $html, string $url = null, string $parser = 'libxml', bool $useTidy = true)
public function __construct(string $html, ?string $url = null, string $parser = 'libxml', bool $useTidy = true)
{
$this->url = $url;
$this->html = $html;
@@ -165,7 +160,7 @@ class Readability implements LoggerAwareInterface
/**
* Get article title element.
*
* @return DOMElement
* @return \DOMElement
*/
public function getTitle()
{
@@ -175,7 +170,7 @@ class Readability implements LoggerAwareInterface
/**
* Get article content element.
*
* @return DOMElement
* @return \DOMElement
*/
public function getContent()
{
@@ -218,7 +213,9 @@ class Readability implements LoggerAwareInterface
*/
public function init(): bool
{
if (!isset($this->dom)) {
$this->loadHtml();
}
if (!isset($this->dom->documentElement)) {
return false;
@@ -284,7 +281,7 @@ class Readability implements LoggerAwareInterface
/**
* Run any post-process modifications to article content as necessary.
*/
public function postProcessContent(DOMElement $articleContent): void
public function postProcessContent(\DOMElement $articleContent): void
{
if ($this->convertLinksToFootnotes && !preg_match('/\bwiki/', $this->url)) {
$this->addFootnotes($articleContent);
@@ -296,7 +293,7 @@ class Readability implements LoggerAwareInterface
*
* @see http://www.roughtype.com/archives/2010/05/experiments_in.php
*/
public function addFootnotes(DOMElement $articleContent): void
public function addFootnotes(\DOMElement $articleContent): void
{
$footnotesWrapper = $this->dom->createElement('footer');
$footnotesWrapper->setAttribute('class', 'readability-footnotes');
@@ -307,8 +304,7 @@ class Readability implements LoggerAwareInterface
$articleLinks = $articleContent->getElementsByTagName('a');
$linkCount = 0;
for ($i = 0; $i < $articleLinks->length; ++$i) {
$articleLink = $articleLinks->item($i);
foreach ($articleLinks as $articleLink) {
$footnoteLink = $articleLink->cloneNode(true);
$refLink = $this->dom->createElement('a');
$footnote = $this->dom->createElement('li');
@@ -339,7 +335,7 @@ class Readability implements LoggerAwareInterface
$articleLink->setAttribute('style', 'color: inherit; text-decoration: none;');
$articleLink->setAttribute('name', 'readabilityLink-' . $linkCount);
$footnote->setInnerHtml('<small><sup><a href="#readabilityLink-' . $linkCount . '" title="Jump to Link in Article">^</a></sup></small> ');
$footnoteLink->setInnerHtml(('' !== $footnoteLink->getAttribute('title') ? $footnoteLink->getAttribute('title') : $linkText));
$footnoteLink->setInnerHtml('' !== $footnoteLink->getAttribute('title') ? $footnoteLink->getAttribute('title') : $linkText);
$footnoteLink->setAttribute('name', 'readabilityFootnoteLink-' . $linkCount);
$footnote->appendChild($footnoteLink);
@@ -360,12 +356,15 @@ class Readability implements LoggerAwareInterface
*/
public function prepArticle(\DOMNode $articleContent): void
{
if (!$articleContent instanceof DOMElement) {
if (!$articleContent instanceof \DOMElement) {
return;
}
$this->logger->debug($this->lightClean ? 'Light clean enabled.' : 'Standard clean enabled.');
$this->clean($articleContent, 'style');
$this->clean($articleContent, 'script');
$this->cleanStyles($articleContent);
$this->killBreaks($articleContent);
@@ -385,8 +384,8 @@ class Readability implements LoggerAwareInterface
// Remove service data-candidate attribute.
$elems = $xpath->query('.//*[@data-candidate]', $articleContent);
for ($i = $elems->length - 1; $i >= 0; --$i) {
$elems->item($i)->removeAttribute('data-candidate');
foreach ($elems as $elem) {
$elem->removeAttribute('data-candidate');
}
// Clean out junk from the article content.
@@ -457,7 +456,7 @@ class Readability implements LoggerAwareInterface
* Get the inner text of a node.
* This also strips out any excess whitespace to be found.
*
* @param DOMElement $e
* @param \DOMElement $e
* @param bool $normalizeSpaces (default: true)
* @param bool $flattenLines (default: false)
*/
@@ -483,7 +482,7 @@ class Readability implements LoggerAwareInterface
/**
* Remove the style attribute on every $e and under.
*/
public function cleanStyles(DOMElement $e): void
public function cleanStyles(\DOMElement $e): void
{
if (\is_object($e)) {
$elems = $e->getElementsByTagName('*');
@@ -516,17 +515,18 @@ class Readability implements LoggerAwareInterface
* 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.
*/
public function getLinkDensity(DOMElement $e, bool $excludeExternal = false): float
public function getLinkDensity(\DOMElement $e, bool $excludeExternal = false): float
{
$links = $e->getElementsByTagName('a');
$textLength = mb_strlen($this->getInnerText($e, true, true));
$linkLength = 0;
for ($dRe = $this->domainRegExp, $i = 0, $il = $links->length; $i < $il; ++$i) {
if ($excludeExternal && $dRe && !preg_match($dRe, $links->item($i)->getAttribute('href'))) {
$dRe = $this->domainRegExp;
foreach ($links as $link) {
if ($excludeExternal && $dRe && !preg_match($dRe, $link->getAttribute('href'))) {
continue;
}
$linkLength += mb_strlen($this->getInnerText($links->item($i)));
$linkLength += mb_strlen($this->getInnerText($link));
}
if ($textLength > 0 && $linkLength > 0) {
@@ -539,7 +539,7 @@ class Readability implements LoggerAwareInterface
/**
* Get an element relative weight.
*/
public function getWeight(DOMElement $e): int
public function getWeight(\DOMElement $e): int
{
if (!$this->flagIsActive(self::FLAG_WEIGHT_ATTRIBUTES)) {
return 0;
@@ -557,7 +557,7 @@ class Readability implements LoggerAwareInterface
/**
* Remove extraneous break tags from a node.
*/
public function killBreaks(DOMElement $node): void
public function killBreaks(\DOMElement $node): void
{
$html = $node->getInnerHTML();
$html = preg_replace($this->regexps['killBreaks'], '<br />', $html);
@@ -570,7 +570,7 @@ class Readability implements LoggerAwareInterface
*
* Updated 2012-09-18 to preserve youtube/vimeo iframes
*/
public function clean(DOMElement $e, string $tag): void
public function clean(\DOMElement $e, string $tag): void
{
$targetList = $e->getElementsByTagName($tag);
$isEmbed = ('audio' === $tag || 'video' === $tag || 'iframe' === $tag || 'object' === $tag || 'embed' === $tag);
@@ -588,7 +588,7 @@ class Readability implements LoggerAwareInterface
}
// Then check the elements inside this element for the same.
if (preg_match($this->regexps['media'], $targetList->item($y)->getInnerHTML())) {
if (preg_match($this->regexps['media'], $currentItem->getInnerHTML())) {
continue;
}
}
@@ -602,7 +602,7 @@ class Readability implements LoggerAwareInterface
* "Fishy" is an algorithm based on content length, classnames,
* link density, number of images & embeds, etc.
*/
public function cleanConditionally(DOMElement $e, string $tag): void
public function cleanConditionally(\DOMElement $e, string $tag): void
{
if (!$this->flagIsActive(self::FLAG_CLEAN_CONDITIONALLY)) {
return;
@@ -642,15 +642,15 @@ class Readability implements LoggerAwareInterface
$embedCount = 0;
$embeds = $node->getElementsByTagName('embed');
for ($ei = 0, $il = $embeds->length; $ei < $il; ++$ei) {
if (preg_match($this->regexps['media'], $embeds->item($ei)->getAttribute('src'))) {
foreach ($embeds as $embed) {
if (preg_match($this->regexps['media'], $embed->getAttribute('src'))) {
++$embedCount;
}
}
$embeds = $node->getElementsByTagName('iframe');
for ($ei = 0, $il = $embeds->length; $ei < $il; ++$ei) {
if (preg_match($this->regexps['media'], $embeds->item($ei)->getAttribute('src'))) {
foreach ($embeds as $embed) {
if (preg_match($this->regexps['media'], $embed->getAttribute('src'))) {
++$embedCount;
}
}
@@ -670,10 +670,10 @@ class Readability implements LoggerAwareInterface
$this->logger->debug(' content length less than 6 chars, 0 embeds and either 0 images or more than 2 images');
$toRemove = true;
} elseif (!$isList && $weight < 25 && $linkDensity > 0.25) {
$this->logger->debug(' weight is ' . $weight . ' < 25 and link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.25');
$this->logger->debug(' weight is ' . $weight . ' < 25 and link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.25');
$toRemove = true;
} elseif ($a > 2 && ($weight >= 25 && $linkDensity > 0.5)) {
$this->logger->debug(' more than 2 links and weight is ' . $weight . ' > 25 but link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.5');
$this->logger->debug(' more than 2 links and weight is ' . $weight . ' > 25 but link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.5');
$toRemove = true;
} elseif ($embedCount > 3) {
$this->logger->debug(' more than 3 embeds');
@@ -693,10 +693,10 @@ class Readability implements LoggerAwareInterface
$this->logger->debug(' content length less than 10 chars and 0 images, or more than 2 images');
$toRemove = true;
} elseif (!$isList && $weight < 25 && $linkDensity > 0.2) {
$this->logger->debug(' weight is ' . $weight . ' lower than 0 and link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.2');
$this->logger->debug(' weight is ' . $weight . ' lower than 0 and link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.2');
$toRemove = true;
} elseif ($weight >= 25 && $linkDensity > 0.5) {
$this->logger->debug(' weight above 25 but link density is ' . sprintf('%.2f', $linkDensity) . ' > 0.5');
$this->logger->debug(' weight above 25 but link density is ' . \sprintf('%.2f', $linkDensity) . ' > 0.5');
$toRemove = true;
} elseif ((1 === $embedCount && $contentLength < 75) || $embedCount > 1) {
$this->logger->debug(' 1 embed and content length smaller than 75 chars, or more than one embed');
@@ -715,14 +715,15 @@ class Readability implements LoggerAwareInterface
/**
* Clean out spurious headers from an Element. Checks things like classnames and link density.
*/
public function cleanHeaders(DOMElement $e): void
public function cleanHeaders(\DOMElement $e): void
{
for ($headerIndex = 1; $headerIndex < 3; ++$headerIndex) {
$headers = $e->getElementsByTagName('h' . $headerIndex);
for ($i = $headers->length - 1; $i >= 0; --$i) {
if ($this->getWeight($headers->item($i)) < 0 || $this->getLinkDensity($headers->item($i)) > 0.33) {
$headers->item($i)->parentNode->removeChild($headers->item($i));
$header = $headers->item($i);
if ($this->getWeight($header) < 0 || $this->getLinkDensity($header) > 0.33) {
$header->parentNode->removeChild($header);
}
}
}
@@ -741,7 +742,7 @@ class Readability implements LoggerAwareInterface
*/
public function addFlag(int $flag): void
{
$this->flags = $this->flags | $flag;
$this->flags |= $flag;
}
/**
@@ -749,13 +750,83 @@ class Readability implements LoggerAwareInterface
*/
public function removeFlag(int $flag): void
{
$this->flags = $this->flags & ~$flag;
$this->flags &= ~$flag;
}
/**
* Load HTML in a DOMDocument.
* Apply Pre filters
* Cleanup HTML using Tidy (or not).
*/
public function loadHtml(): void
{
$this->original_html = $this->html;
$this->logger->debug('Parsing URL: ' . $this->url);
if ($this->url) {
$host = parse_url($this->url, \PHP_URL_HOST);
if (null !== $host) {
$this->domainRegExp = '/' . strtr((string) preg_replace('/www\d*\./', '', $host), ['.' => '\.']) . '/';
}
}
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');
mb_regex_encoding('UTF-8');
// HACK: dirty cleanup to replace some stuff; shouldn't use regexps with HTML but well...
if (!$this->flagIsActive(self::FLAG_DISABLE_PREFILTER)) {
foreach ($this->pre_filters as $search => $replace) {
$this->html = preg_replace($search, $replace, $this->html);
}
unset($search, $replace);
}
if ('' === trim($this->html)) {
$this->html = '<html></html>';
}
/*
* Use tidy (if it exists).
* This fixes problems with some sites which would otherwise trouble DOMDocument's HTML parsing.
* Although sometimes it makes matters worse, which is why there is an option to disable it.
*/
if ($this->useTidy) {
$this->logger->debug('Tidying document');
$tidy = tidy_repair_string($this->html, $this->tidy_config, 'UTF8');
if (false !== $tidy && $this->html !== $tidy) {
$this->tidied = true;
$this->html = $tidy;
$this->html = preg_replace('/[\r\n]+/is', "\n", $this->html);
}
unset($tidy);
}
$this->html = self::entitizeNonAscii((string) $this->html);
if ('html5lib' === $this->parser || 'html5' === $this->parser) {
$this->dom = (new HTML5())->loadHTML($this->html);
}
if ('libxml' === $this->parser) {
libxml_use_internal_errors(true);
$this->dom = new \DOMDocument();
$this->dom->preserveWhiteSpace = false;
$this->dom->loadHTML($this->html, \LIBXML_NOBLANKS | \LIBXML_COMPACT | \LIBXML_NOERROR);
libxml_use_internal_errors(false);
}
$this->dom->registerNodeClass(\DOMElement::class, JSLikeHTMLElement::class);
}
/**
* Get the article title as an H1.
*
* @return DOMElement
* @return \DOMElement
*/
protected function getArticleTitle()
{
@@ -814,12 +885,14 @@ class Readability implements LoggerAwareInterface
// Remove all style tags in head.
$styleTags = $this->dom->getElementsByTagName('style');
for ($i = $styleTags->length - 1; $i >= 0; --$i) {
$styleTags->item($i)->parentNode->removeChild($styleTags->item($i));
$styleTag = $styleTags->item($i);
$styleTag->parentNode->removeChild($styleTag);
}
$linkTags = $this->dom->getElementsByTagName('link');
for ($i = $linkTags->length - 1; $i >= 0; --$i) {
$linkTags->item($i)->parentNode->removeChild($linkTags->item($i));
$linkTag = $linkTags->item($i);
$linkTag->parentNode->removeChild($linkTag);
}
}
@@ -827,7 +900,7 @@ class Readability implements LoggerAwareInterface
* Initialize a node with the readability object. Also checks the
* className/id for special names to add to its score.
*/
protected function initializeNode(DOMElement $node): void
protected function initializeNode(\DOMElement $node): void
{
if (!isset($node->tagName)) {
return;
@@ -895,11 +968,9 @@ class Readability implements LoggerAwareInterface
* Using a variety of metrics (content score, classname, element types), find the content that is
* most likely to be the stuff a user wants to read. Then return it wrapped up in a div.
*
* @param DOMElement $page
*
* @return DOMElement|false
* @return \DOMElement|false
*/
protected function grabArticle(DOMElement $page = null)
protected function grabArticle(?\DOMElement $page = null)
{
if (!$page) {
$page = $this->dom;
@@ -935,9 +1006,9 @@ class Readability implements LoggerAwareInterface
// Remove unlikely candidates
$unlikelyMatchString = $node->getAttribute('class') . ' ' . $node->getAttribute('id') . ' ' . $node->getAttribute('style');
if (mb_strlen($unlikelyMatchString) > 3 && // don't process "empty" strings
preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString) &&
!preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
if (mb_strlen($unlikelyMatchString) > 3 // don't process "empty" strings
&& preg_match($this->regexps['unlikelyCandidates'], $unlikelyMatchString)
&& !preg_match($this->regexps['okMaybeItsACandidate'], $unlikelyMatchString)
) {
$this->logger->debug('Removing unlikely candidate (using conf) ' . $node->getNodePath() . ' by "' . $unlikelyMatchString . '"');
$node->parentNode->removeChild($node);
@@ -1004,7 +1075,11 @@ class Readability implements LoggerAwareInterface
}
if ($this->hasSingleTagInsideElement($node, 'p') && $this->getLinkDensity($node) < 0.25) {
$newNode = $node->childNodes->item(0);
// In some cases when tidy is disabled the first item may not be a DOMElement so we apply a filter
$newNode = array_values(array_filter(
iterator_to_array($node->childNodes),
static fn ($childNode) => $childNode instanceof \DOMElement
))[0];
$node->parentNode->replaceChild($newNode, $node);
$nodesToScore[] = $newNode;
}
@@ -1019,15 +1094,15 @@ class Readability implements LoggerAwareInterface
* A score is determined by things like number of commas, class names, etc.
* Maybe eventually link density.
*/
for ($pt = 0, $scored = \count($nodesToScore); $pt < $scored; ++$pt) {
$ancestors = $this->getAncestors($nodesToScore[$pt], 5);
foreach ($nodesToScore as $nodeToScore) {
$ancestors = $this->getAncestors($nodeToScore, 5);
// No parent node? Move on...
if (0 === \count($ancestors)) {
continue;
}
$innerText = $this->getInnerText($nodesToScore[$pt]);
$innerText = $this->getInnerText($nodeToScore);
// If this paragraph is less than MIN_PARAGRAPH_LENGTH (default:20) characters, don't even count it.
if (mb_strlen($innerText) < self::MIN_PARAGRAPH_LENGTH) {
@@ -1080,11 +1155,6 @@ class Readability implements LoggerAwareInterface
}
}
$candidates = $xpath->query('.//*[not(self::body) and (@class or @id or @style) and ((number(@readability) < 40) or not(@readability))]', $page->documentElement);
for ($c = $candidates->length - 1; $c >= 0; --$c) {
$node = $candidates->item($c);
}
unset($candidates);
}
@@ -1122,9 +1192,11 @@ class Readability implements LoggerAwareInterface
}
}
$topCandidates = array_filter($topCandidates, function ($v, $idx) {
return 0 === $idx || null !== $v;
}, \ARRAY_FILTER_USE_BOTH);
$topCandidates = array_filter(
$topCandidates,
static fn ($v, $idx) => 0 === $idx || null !== $v,
\ARRAY_FILTER_USE_BOTH
);
$topCandidate = $topCandidates[0];
/*
@@ -1212,7 +1284,7 @@ class Readability implements LoggerAwareInterface
if (0 === strcasecmp($tagName, 'td') || 0 === strcasecmp($tagName, 'tr')) {
$up = $topCandidate;
if ($up->parentNode instanceof DOMElement) {
if ($up->parentNode instanceof \DOMElement) {
$up = $up->parentNode;
if (0 === strcasecmp($up->tagName, 'table')) {
@@ -1233,11 +1305,6 @@ class Readability implements LoggerAwareInterface
$parentOfTopCandidate = $topCandidate->parentNode;
$siblingNodes = $parentOfTopCandidate->childNodes;
if (0 === $siblingNodes->length) {
$siblingNodes = new \stdClass();
$siblingNodes->length = 0;
}
for ($s = 0, $sl = $siblingNodes->length; $s < $sl; ++$s) {
$siblingNode = $siblingNodes->item($s);
$siblingNodeName = $siblingNode->nodeName;
@@ -1341,7 +1408,7 @@ class Readability implements LoggerAwareInterface
* Get an element weight by attribute.
* Uses regular expressions to tell if this element looks good or bad.
*/
protected function weightAttribute(DOMElement $element, string $attribute): int
protected function weightAttribute(\DOMElement $element, string $attribute): int
{
if (!$element->hasAttribute($attribute)) {
return 0;
@@ -1380,78 +1447,11 @@ class Readability implements LoggerAwareInterface
}
}
/**
* Load HTML in a DOMDocument.
* Apply Pre filters
* Cleanup HTML using Tidy (or not).
*/
private function loadHtml(): void
{
$this->original_html = $this->html;
$this->logger->debug('Parsing URL: ' . $this->url);
if ($this->url) {
$this->domainRegExp = '/' . strtr((string) preg_replace('/www\d*\./', '', (string) parse_url($this->url, \PHP_URL_HOST)), ['.' => '\.']) . '/';
}
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');
mb_regex_encoding('UTF-8');
// HACK: dirty cleanup to replace some stuff; shouldn't use regexps with HTML but well...
if (!$this->flagIsActive(self::FLAG_DISABLE_PREFILTER)) {
foreach ($this->pre_filters as $search => $replace) {
$this->html = preg_replace($search, $replace, $this->html);
}
unset($search, $replace);
}
if ('' === trim($this->html)) {
$this->html = '<html></html>';
}
/*
* Use tidy (if it exists).
* This fixes problems with some sites which would otherwise trouble DOMDocument's HTML parsing.
* Although sometimes it makes matters worse, which is why there is an option to disable it.
*/
if ($this->useTidy) {
$this->logger->debug('Tidying document');
$tidy = tidy_repair_string($this->html, $this->tidy_config, 'UTF8');
if (false !== $tidy && $this->html !== $tidy) {
$this->tidied = true;
$this->html = $tidy;
$this->html = preg_replace('/[\r\n]+/is', "\n", $this->html);
}
unset($tidy);
}
$this->html = mb_convert_encoding((string) $this->html, 'HTML-ENTITIES', 'UTF-8');
if ('html5lib' === $this->parser || 'html5' === $this->parser) {
$this->dom = (new HTML5())->loadHTML($this->html);
}
if ('libxml' === $this->parser) {
libxml_use_internal_errors(true);
$this->dom = new \DOMDocument();
$this->dom->preserveWhiteSpace = false;
$this->dom->loadHTML($this->html, \LIBXML_NOBLANKS | \LIBXML_COMPACT | \LIBXML_NOERROR);
libxml_use_internal_errors(false);
}
$this->dom->registerNodeClass(DOMElement::class, \Readability\JSLikeHTMLElement::class);
}
private function getAncestors(DOMElement $node, int $maxDepth = 0): array
private function getAncestors(\DOMElement $node, int $maxDepth = 0): array
{
$ancestors = [];
$i = 0;
while ($node->parentNode instanceof DOMElement) {
while ($node->parentNode instanceof \DOMElement) {
$ancestors[] = $node->parentNode;
if (++$i === $maxDepth) {
break;
@@ -1466,21 +1466,45 @@ class Readability implements LoggerAwareInterface
{
return \XML_TEXT_NODE === $node->nodeType
|| \in_array(strtoupper($node->nodeName), $this->phrasingElements, true)
|| (\in_array(strtoupper($node->nodeName), ['A', 'DEL', 'INS'], true) && !\in_array(false, array_map(function ($c) {
return $this->isPhrasingContent($c);
}, iterator_to_array($node->childNodes)), true));
|| (
\in_array(strtoupper($node->nodeName), ['A', 'DEL', 'INS'], true)
&& !\in_array(
false,
array_map(
fn ($c) => $this->isPhrasingContent($c),
iterator_to_array($node->childNodes)
),
true
)
);
}
private function hasSingleTagInsideElement(DOMElement $node, string $tag): bool
/**
* Checks if `$node` has only whitespace and a single element with `$tag` for the tag name.
* Returns false if `$node` contains non-empty text nodes
* or if it contains no element with given tag or more than 1 element.
*/
private function hasSingleTagInsideElement(\DOMElement $node, string $tag): bool
{
if (1 !== $node->childNodes->length || $node->childNodes->item(0)->nodeName !== $tag) {
$childNodes = iterator_to_array($node->childNodes);
$children = array_filter($childNodes, static fn ($childNode) => $childNode instanceof \DOMElement);
// array_first() has been added in PHP 8.5, failing back for older versions
if (!\function_exists('array_first')) {
$firstChild = $children ? $children[array_key_first($children)] : null;
} else {
$firstChild = array_first($children);
}
// There should be exactly 1 element child with given tag
if (1 !== \count($children) || $firstChild->nodeName !== $tag) {
return false;
}
$a = array_filter(iterator_to_array($node->childNodes), function ($childNode) {
return $childNode instanceof \DOMText &&
preg_match($this->regexps['hasContent'], $this->getInnerText($childNode));
});
$a = array_filter(
$childNodes,
fn ($childNode) => $childNode instanceof \DOMText && preg_match($this->regexps['hasContent'], $this->getInnerText($childNode))
);
return 0 === \count($a);
}
@@ -1491,11 +1515,29 @@ class Readability implements LoggerAwareInterface
* Tidy must be configured to not clean the input for this function to
* work as expected, see $this->tidy_config['clean']
*/
private function isNodeVisible(DOMElement $node): bool
private function isNodeVisible(\DOMElement $node): bool
{
return !($node->hasAttribute('style')
return !(
$node->hasAttribute('style')
&& preg_match($this->regexps['isNotVisible'], $node->getAttribute('style'))
)
&& !$node->hasAttribute('hidden');
}
/**
* Converts non-ASCII UTF-8 characters to numeric HTML entities.
*
* `DOMDocument::loadHTML` will parse HTML documents as ISO-8859-1 if there is no `meta[charset]` tag.
* This means that UTF-8-encoded HTML fragments such as those coming from JSON-LD `articleBody` field would be parsed with incorrect encoding.
*
* @param string $html UTF-8 encoded document
*/
private static function entitizeNonAscii(string $html): string
{
$convmap = [
0x80, 0x1FFFFF, 0, 0x10FFFF,
];
return mb_encode_numericentity($html, $convmap, 'utf8', true);
}
}
+133 -31
View File
@@ -21,10 +21,11 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testConstructDefault(): void
{
$readability = $this->getReadability('');
$this->assertSame('utf8', $readability->tidy_config['input-encoding']);
$readability->init();
$this->assertNull($readability->url);
$this->assertInstanceOf('DomDocument', $readability->dom);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
}
public function testConstructHtml5Parser(): void
@@ -33,7 +34,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$readability->init();
$this->assertSame('http://0.0.0.0', $readability->url);
$this->assertInstanceOf('DomDocument', $readability->dom);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
$this->assertSame('<html/>', $readability->original_html);
}
@@ -46,7 +47,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$readability->init();
$this->assertSame('http://0.0.0.0', $readability->url);
$this->assertInstanceOf('DomDocument', $readability->dom);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
$this->assertSame('<html/>', $readability->original_html);
$this->assertTrue($readability->tidied);
}
@@ -60,7 +61,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$this->assertSame('', $readability->original_html);
$this->assertFalse($readability->tidied);
$this->assertInstanceOf('DomDocument', $readability->dom);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
}
public function testConstructSimpleWithoutTidy(): void
@@ -69,7 +70,7 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$readability->init();
$this->assertSame('http://0.0.0.0', $readability->url);
$this->assertInstanceOf('DomDocument', $readability->dom);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
$this->assertSame('<html/>', $readability->original_html);
$this->assertFalse($readability->tidied);
}
@@ -115,7 +116,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testInitDiv(): void
{
$readability = $this->getReadability('<div>' . str_repeat('This is the awesome content :)', 7) . '</div>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -129,7 +129,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithFootnotes(): void
{
$readability = $this->getReadability('<div>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '</div>', 'http://0.0.0.0');
$readability->debug = true;
$readability->convertLinksToFootnotes = true;
$res = $readability->init();
@@ -146,7 +145,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testStandardClean(): void
{
$readability = $this->getReadability('<div><h2>Title</h2>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<a href="#nofollow" rel="nofollow">will NOT be removed</a></div>', 'http://0.0.0.0');
$readability->debug = true;
$readability->lightClean = false;
$res = $readability->init();
@@ -163,7 +161,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithIframe(): void
{
$readability = $this->getReadability('<div><h2>Title</h2>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<p>This is an awesome text with some links, here there are <iframe src="http://youtube.com/test" href="#nofollow" rel="nofollow"></iframe><iframe>http://soundcloud.com/test</iframe></p></div>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -178,7 +175,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithArticle(): void
{
$readability = $this->getReadability('<article><p>' . str_repeat('This is an awesome text with some links, here there are: the awesome', 20) . '</p><p>This is an awesome text with some links, here there are <iframe src="http://youtube.com/test" href="#nofollow" rel="nofollow"></iframe></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -193,7 +189,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithAside(): void
{
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<footer><aside>' . str_repeat('<p>This is an awesome text with some links, here there are</p>', 8) . '</aside></footer></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -208,7 +203,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithClasses(): void
{
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<div style="display:none">' . str_repeat('<p class="clock">This text should be removed</p>', 10) . '</div></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -223,7 +217,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithClassesWithoutLightClean(): void
{
$readability = $this->getReadability('<article>' . str_repeat('<p>This is an awesome text with some links, here there are: <a href="http://0.0.0.0/test.html">the awesome</a></p>', 7) . '<div style="display:none">' . str_repeat('<p class="clock">This text should be removed</p>', 10) . '</div></article>', 'http://0.0.0.0');
$readability->debug = true;
$readability->lightClean = false;
$res = $readability->init();
@@ -239,7 +232,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithTd(): void
{
$readability = $this->getReadability('<table><tr>' . str_repeat('<td><p>This is an awesome text with some links, here there are the awesome</td>', 7) . '</tr></table>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -252,7 +244,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithSameClasses(): void
{
$readability = $this->getReadability('<article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<div class="awesomecontent">This text is also an awesome text and you should know that !</div></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -266,7 +257,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testWithScript(): void
{
$readability = $this->getReadability('<article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p><script>This text is also an awesome text and you should know that !</script></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -280,7 +270,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testTitle(): void
{
$readability = $this->getReadability('<title>this is my title</title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -294,7 +283,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testTitleWithDash(): void
{
$readability = $this->getReadability('<title> title2 - title3 </title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -308,7 +296,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testTitleWithDoubleDot(): void
{
$readability = $this->getReadability('<title> title2 : title3 </title><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -322,7 +309,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testTitleTooShortUseH1(): void
{
$readability = $this->getReadability('<title>too short</title><h1>this is my h1 title !</h1><article class="awesomecontent">' . str_repeat('<p>This is an awesome text with some links, here there are the awesome</p>', 7) . '<p></p></article>', 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -335,13 +321,14 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testAutoClosingIframeNotThrowingException(): void
{
error_reporting(\E_ALL | \E_STRICT);
ini_set('display_errors', '1');
$oldErrorReporting = error_reporting(\E_ALL);
$oldDisplayErrors = ini_set('display_errors', '1');
// dummy function to be used to the next test
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline, array $errcontext) {
set_error_handler(static function (int $errno, string $errstr, string $errfile, int $errline) {
throw new \Exception($errstr, $errno);
}, \E_ALL | \E_STRICT);
});
try {
$data = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ru-RU" prefix="og: http://ogp.me/ns#">
@@ -368,7 +355,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
</html>';
$readability = $this->getReadability($data, 'http://iosgames.ru/?p=22030');
$readability->debug = true;
$res = $readability->init();
@@ -377,6 +363,13 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getTitle());
$this->assertStringContainsString('<iframe src="https://www.youtube.com/embed/PUep6xNeKjA" width="560" height="315" frameborder="0" allowfullscreen="allowfullscreen"> </iframe>', $readability->getContent()->getInnerHtml());
$this->assertStringContainsString('3D Touch', $readability->getTitle()->getInnerHtml());
} finally {
restore_error_handler();
if (false !== $oldDisplayErrors) {
ini_set('display_errors', $oldDisplayErrors);
}
error_reporting($oldErrorReporting);
}
}
/**
@@ -429,7 +422,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
</html>';
$readability = $this->getReadability($data, 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
@@ -466,20 +458,74 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$html = (string) file_get_contents('tests/fixtures/childNodeGoesNull.html');
$readability = $this->getReadability($html, 'http://0.0.0.0');
$readability->debug = true;
$readability->convertLinksToFootnotes = true;
$res = $readability->init();
$this->assertTrue($res);
}
public function dataForHasSingleTagInsideElement(): array
{
return [
'single matching tag, no other content' => [
'<div><p>Some text</p></div>', 'p', true,
],
'single matching tag with comment' => [
'<div><!-- comment --><p>Some text</p></div>', 'p', true,
],
'whitespace-only text around single matching tag' => [
'<div> <p>Some text</p> </div>', 'p', true,
],
'two matching tags' => [
'<div><p>One</p><p>Two</p></div>', 'p', false,
],
'non-whitespace text alongside the single tag' => [
'<div>Some text<p>One</p></div>', 'p', false,
],
'single tag with a different name' => [
'<div><span>One</span></div>', 'p', false,
],
];
}
/**
* @dataProvider dataForHasSingleTagInsideElement
*/
public function testHasSingleTagInsideElement(string $html, string $tag, bool $expected): void
{
$dom = new \DOMDocument();
$dom->loadHTML('<html><body>' . $html . '</body></html>');
$node = $dom->getElementsByTagName('div')->item(0);
$readability = $this->getReadability('', '', 'libxml', false);
$method = new \ReflectionMethod($readability, 'hasSingleTagInsideElement');
if (\PHP_VERSION_ID < 80100) {
$method->setAccessible(true);
}
$this->assertSame($expected, $method->invoke($readability, $node, $tag));
}
public function testDivToPElementWithComment(): void
{
$text = str_repeat('Padded real article text to reach decent length for content scoring threshold here. ', 8);
$html = '<div><!-- comment --><code>some code snippet</code> ' . $text . '</div>';
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', false);
$res = $readability->init();
$this->assertTrue($res);
$this->assertStringContainsString('some code snippet', $readability->getContent()->getInnerHtml());
$this->assertStringContainsString('Padded real article text', $readability->getContent()->getInnerHtml());
}
public function testKeepFootnotes(): void
{
// from https://www.schreibdichte.de/blog/feed-aggregator-und-spaeter-lesen-dienst-im-team
$html = (string) file_get_contents('tests/fixtures/keepFootnotes.html');
$readability = $this->getReadability($html, 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -493,7 +539,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
$html = (string) file_get_contents('tests/fixtures/wipedBody.html');
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', false);
$readability->debug = true;
$res = $readability->init();
$this->assertTrue($res);
@@ -532,7 +577,6 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
public function testVisibleNode(string $content, bool $shouldBeVisible): void
{
$readability = $this->getReadability($content, 'http://0.0.0.0');
$readability->debug = true;
$res = $readability->init();
if ($shouldBeVisible) {
@@ -542,7 +586,65 @@ class ReadabilityTest extends \PHPUnit\Framework\TestCase
}
}
private function getReadability(string $html, string $url = null, string $parser = 'libxml', bool $useTidy = true): Readability
// https://github.com/wallabag/wallabag/issues/8158
public function testCharsetAfterTitle(): void
{
$readability = $this->getReadability('<!DOCTYPE html><html lang="et"><head><title>Tõde ja õigus I</title> <meta charset="utf-8"></head><body><p>See oli läinud aastasaja kolmanda veerandi lõpul. Päike lähenes silmapiirile, seistes sedavõrd madalas, et enam ei ulatunud valgustama ei mäkke ronivat hobust, kes puutelgedega vankrit vedas, ei vankril istuvat noort naist ega ka ligi kolmekümnelist meest, kes kõndis vankri kõrval.</p></body></html>', 'https://et.wikisource.org/wiki/T%C3%B5de_ja_%C3%B5igus_I/I');
$readability->convertLinksToFootnotes = true;
$res = $readability->init();
$this->assertTrue($res);
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getContent());
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getTitle());
$this->assertSame('Tõde ja õigus I', $readability->getTitle()->getInnerHtml());
$this->assertStringContainsString('Päike lähenes', $readability->getContent()->getInnerHtml());
}
/**
* @return array<string, array{0: string, 1: string, 2?: bool}>
*/
public function dataForHtmlLang(): array
{
return [
'meta' => [
'<html lang="fr"><head><meta charset="utf-8"></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr',
],
'head' => [
'<html lang="fr"><head><title>Foo</title></head><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr',
],
'headless' => [
'<html lang="fr"><body><article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article></body></html>',
'fr',
// tidy would add <head> tag.
false,
],
'fragment' => [
'<article>' . str_repeat('<p>Tous les êtres humains naissent libres et égaux en dignité et en droits. Ils sont doués de raison et de conscience et doivent agir les uns envers les autres dans un esprit de fraternité.</p>', 7) . '</article>',
'',
// tidy would add <html>.
false,
],
];
}
/**
* @dataProvider dataForHtmlLang
*/
public function testHtmlLang(string $html, string $lang, bool $useTidy = true): void
{
$readability = $this->getReadability($html, 'http://0.0.0.0', 'libxml', $useTidy);
$res = $readability->init();
$this->assertTrue($res);
$this->assertInstanceOf(\DOMDocument::class, $readability->dom);
$this->assertSame($lang, $readability->dom->documentElement->getAttribute('lang'));
$this->assertInstanceOf(JSLikeHTMLElement::class, $readability->getContent());
$this->assertStringContainsString('êtres', $readability->getContent()->getInnerHtml());
}
private function getReadability(string $html, ?string $url = null, string $parser = 'libxml', bool $useTidy = true): Readability
{
$readability = new Readability($html, $url, $parser, $useTidy);