🔧
This commit is contained in:
2
vendor/league/commonmark/.phpstorm.meta.php
vendored
2
vendor/league/commonmark/.phpstorm.meta.php
vendored
@@ -31,6 +31,7 @@ namespace PHPSTORM_META
|
||||
'html_input',
|
||||
'allow_unsafe_links',
|
||||
'max_nesting_level',
|
||||
'max_delimiters_per_line',
|
||||
'renderer',
|
||||
'renderer/block_separator',
|
||||
'renderer/inner_separator',
|
||||
@@ -89,6 +90,7 @@ namespace PHPSTORM_META
|
||||
'table/alignment_attributes/left',
|
||||
'table/alignment_attributes/center',
|
||||
'table/alignment_attributes/right',
|
||||
'table/max_autocompleted_cells',
|
||||
'table_of_contents',
|
||||
'table_of_contents/html_class',
|
||||
'table_of_contents/max_heading_level',
|
||||
|
||||
153
vendor/league/commonmark/CHANGELOG.md
vendored
153
vendor/league/commonmark/CHANGELOG.md
vendored
@@ -6,6 +6,146 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
|
||||
|
||||
## [Unreleased][unreleased]
|
||||
|
||||
## [2.7.0]
|
||||
|
||||
This is a **security release** to address a potential cross-site scripting (XSS) vulnerability when using the `AttributesExtension` with untrusted user input.
|
||||
|
||||
### Added
|
||||
- Added `attributes/allow` config option to specify which attributes users are allowed to set on elements (default allows virtually all attributes)
|
||||
|
||||
### Changed
|
||||
- The `AttributesExtension` blocks all attributes starting with `on` unless explicitly allowed via the `attributes/allow` config option
|
||||
- The `allow_unsafe_links` option is now respected by the `AttributesExtension` when users specify `href` and `src` attributes
|
||||
|
||||
## [2.6.2] - 2025-04-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Attributes extension parsing regression (#1071)
|
||||
|
||||
## [2.6.1] - 2024-12-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rendered list items should only add newlines around block-level children (#1059, #1061)
|
||||
|
||||
## [2.6.0] - 2024-12-07
|
||||
|
||||
This is a **security release** to address potential denial of service attacks when parsing specially crafted,
|
||||
malicious input from untrusted sources (like user input).
|
||||
|
||||
### Added
|
||||
|
||||
- Added `max_delimiters_per_line` config option to prevent denial of service attacks when parsing malicious input
|
||||
- Added `table/max_autocompleted_cells` config option to prevent denial of service attacks when parsing large tables
|
||||
- The `AttributesExtension` now supports attributes without values (#985, #986)
|
||||
- The `AutolinkExtension` exposes two new configuration options to override the default behavior (#969, #987):
|
||||
- `autolink/allowed_protocols` - an array of protocols to allow autolinking for
|
||||
- `autolink/default_protocol` - the default protocol to use when none is specified
|
||||
- Added `RegexHelper::isWhitespace()` method to check if a given character is an ASCII whitespace character
|
||||
- Added `CacheableDelimiterProcessorInterface` to ensure linear complexity for dynamic delimiter processing
|
||||
- Added `Bracket` delimiter type to optimize bracket parsing
|
||||
|
||||
### Changed
|
||||
|
||||
- `[` and `]` are no longer added as `Delimiter` objects on the stack; a new `Bracket` type with its own stack is used instead
|
||||
- `UrlAutolinkParser` no longer parses URLs with more than 127 subdomains
|
||||
- Expanded reference links can no longer exceed 100kb, or the size of the input document (whichever is greater)
|
||||
- Delimiters should always provide a non-null value via `DelimiterInterface::getIndex()`
|
||||
- We'll attempt to infer the index based on surrounding delimiters where possible
|
||||
- The `DelimiterStack` now accepts integer positions for any `$stackBottom` argument
|
||||
- Several small performance optimizations
|
||||
|
||||
## [2.5.3] - 2024-08-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Made compatible with CommonMark spec 0.31.1, including:
|
||||
- Remove `source`, add `search` to list of recognized block tags
|
||||
|
||||
## [2.5.2] - 2024-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Boolean attributes now require an explicit `true` value (#1040)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed regression where text could be misinterpreted as an attribute (#1040)
|
||||
|
||||
## [2.5.1] - 2024-07-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed attribute parsing incorrectly parsing mustache-like syntax (#1035)
|
||||
- Fixed incorrect `Table` start line numbers (#1037)
|
||||
|
||||
## [2.5.0] - 2024-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- The `AttributesExtension` now supports attributes without values (#985, #986)
|
||||
- The `AutolinkExtension` exposes two new configuration options to override the default behavior (#969, #987):
|
||||
- `autolink/allowed_protocols` - an array of protocols to allow autolinking for
|
||||
- `autolink/default_protocol` - the default protocol to use when none is specified
|
||||
|
||||
### Changed
|
||||
|
||||
- Made compatible with CommonMark spec 0.31.0, including:
|
||||
- Allow closing fence to be followed by tabs
|
||||
- Remove restrictive limitation on inline comments
|
||||
- Unicode symbols now treated like punctuation (for purposes of flankingness)
|
||||
- Trailing tabs on the last line of indented code blocks will be excluded
|
||||
- Improved HTML comment matching
|
||||
- `Paragraph`s only containing link reference definitions will be kept in the AST until the `Document` is finalized
|
||||
- (These were previously removed immediately after parsing the `Paragraph`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed list tightness not being determined properly in some edge cases
|
||||
- Fixed incorrect ending line numbers for several block types in various scenarios
|
||||
- Fixed lowercase inline HTML declarations not being accepted
|
||||
|
||||
## [2.4.4] - 2024-07-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed SmartPunct extension changing already-formatted quotation marks (#1030)
|
||||
|
||||
## [2.4.3] - 2024-07-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the Attributes extension not supporting CSS level 3 selectors (#1013)
|
||||
- Fixed `UrlAutolinkParser` incorrectly parsing text containing `www` anywhere before an autolink (#1025)
|
||||
|
||||
|
||||
## [2.4.2] - 2024-02-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed declaration parser being too strict
|
||||
- `FencedCodeRenderer`: don't add `language-` to class if already prefixed
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Returning dynamic values from `DelimiterProcessorInterface::getDelimiterUse()` is deprecated
|
||||
- You should instead implement `CacheableDelimiterProcessorInterface` to help the engine perform caching to avoid performance issues.
|
||||
- Failing to set a delimiter's index (or returning `null` from `DelimiterInterface::getIndex()`) is deprecated and will not be supported in 3.0
|
||||
- Deprecated `DelimiterInterface::isActive()` and `DelimiterInterface::setActive()`, as these are no longer used by the engine
|
||||
- Deprecated `DelimiterStack::removeEarlierMatches()` and `DelimiterStack::searchByCharacter()`, as these are no longer used by the engine
|
||||
- Passing a `DelimiterInterface` as the `$stackBottom` argument to `DelimiterStack::processDelimiters()` or `::removeAll()` is deprecated and will not be supported in 3.0; pass the integer position instead.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed NUL characters not being replaced in the input
|
||||
- Fixed quadratic complexity parsing unclosed inline links
|
||||
- Fixed quadratic complexity parsing emphasis and strikethrough delimiters
|
||||
- Fixed issue where having 500,000+ delimiters could trigger a [known segmentation fault issue in PHP's garbage collection](https://bugs.php.net/bug.php?id=68606)
|
||||
- Fixed quadratic complexity deactivating link openers
|
||||
- Fixed quadratic complexity parsing long backtick code spans with no matching closers
|
||||
- Fixed catastrophic backtracking when parsing link labels/titles
|
||||
|
||||
## [2.4.1] - 2023-08-30
|
||||
|
||||
### Fixed
|
||||
@@ -560,7 +700,18 @@ No changes were introduced since the previous release.
|
||||
- Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment
|
||||
- Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself
|
||||
|
||||
[unreleased]: https://github.com/thephpleague/commonmark/compare/2.4.1...main
|
||||
[unreleased]: https://github.com/thephpleague/commonmark/compare/2.7.0...HEAD
|
||||
[2.7.0]: https://github.com/thephpleague/commonmark/compare/2.6.2...2.7.0
|
||||
[2.6.2]: https://github.com/thephpleague/commonmark/compare/2.6.1...2.6.2
|
||||
[2.6.1]: https://github.com/thephpleague/commonmark/compare/2.6.0...2.6.1
|
||||
[2.6.0]: https://github.com/thephpleague/commonmark/compare/2.5.3...2.6.0
|
||||
[2.5.3]: https://github.com/thephpleague/commonmark/compare/2.5.2...2.5.3
|
||||
[2.5.2]: https://github.com/thephpleague/commonmark/compare/2.5.1...2.5.2
|
||||
[2.5.1]: https://github.com/thephpleague/commonmark/compare/2.5.0...2.5.1
|
||||
[2.5.0]: https://github.com/thephpleague/commonmark/compare/2.4.4...2.5.0
|
||||
[2.4.4]: https://github.com/thephpleague/commonmark/compare/2.4.3...2.4.4
|
||||
[2.4.3]: https://github.com/thephpleague/commonmark/compare/2.4.2...2.4.3
|
||||
[2.4.2]: https://github.com/thephpleague/commonmark/compare/2.4.1...2.4.2
|
||||
[2.4.1]: https://github.com/thephpleague/commonmark/compare/2.4.0...2.4.1
|
||||
[2.4.0]: https://github.com/thephpleague/commonmark/compare/2.3.9...2.4.0
|
||||
[2.3.9]: https://github.com/thephpleague/commonmark/compare/2.3.8...2.3.9
|
||||
|
||||
16
vendor/league/commonmark/README.md
vendored
16
vendor/league/commonmark/README.md
vendored
@@ -54,7 +54,8 @@ echo $converter->convert('# Hello World!');
|
||||
|
||||
Please note that only UTF-8 and ASCII encodings are supported. If your Markdown uses a different encoding please convert it to UTF-8 before running it through this library.
|
||||
|
||||
🔒 If you will be parsing untrusted input from users, please consider setting the `html_input` and `allow_unsafe_links` options per the example above. See <https://commonmark.thephpleague.com/security/> for more details. If you also do choose to allow raw HTML input from untrusted users, consider using a library (like [HTML Purifier](https://github.com/ezyang/htmlpurifier)) to provide additional HTML filtering.
|
||||
> [!CAUTION]
|
||||
> If you will be parsing untrusted input from users, please consider setting the `html_input` and `allow_unsafe_links` options per the example above. See <https://commonmark.thephpleague.com/security/> for more details. If you also do choose to allow raw HTML input from untrusted users, consider using a library (like [HTML Purifier](https://github.com/ezyang/htmlpurifier)) to provide additional HTML filtering.
|
||||
|
||||
## 📓 Documentation
|
||||
|
||||
@@ -98,7 +99,7 @@ See [our extension documentation](https://commonmark.thephpleague.com/extensions
|
||||
|
||||
Custom parsers/renderers can be bundled into extensions which extend CommonMark. Here are some that you may find interesting:
|
||||
|
||||
- [Alt Three Emoji](https://github.com/AltThree/Emoji) An emoji parser for CommonMark.
|
||||
- [Emoji extension](https://github.com/ElGigi/CommonMarkEmoji) - UTF-8 emoji extension with Github tag.
|
||||
- [Sup Sub extensions](https://github.com/OWS/commonmark-sup-sub-extensions) - Adds support of superscript and subscript (`<sup>` and `<sub>` HTML tags)
|
||||
- [YouTube iframe extension](https://github.com/zoonru/commonmark-ext-youtube-iframe) - Replaces youtube link with iframe.
|
||||
- [Lazy Image extension](https://github.com/simonvomeyser/commonmark-ext-lazy-image) - Adds various options for lazy loading of images.
|
||||
@@ -163,11 +164,13 @@ $ ./tests/benchmark/benchmark.php
|
||||
|
||||
## 👥 Credits & Acknowledgements
|
||||
|
||||
- [Colin O'Dell][@colinodell]
|
||||
- [John MacFarlane][@jgm]
|
||||
- [All Contributors]
|
||||
This code was originally based on the [CommonMark JS reference implementation][commonmark.js] which is written, maintained, and copyrighted by [John MacFarlane]. This project simply wouldn't exist without his work.
|
||||
|
||||
This code is partially based on the [CommonMark JS reference implementation][commonmark.js] which is written, maintained and copyrighted by [John MacFarlane]. This project simply wouldn't exist without his work.
|
||||
And a huge thanks to all of our amazing contributors:
|
||||
|
||||
<a href="https://github.com/thephpleague/commonmark/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=thephpleague/commonmark" />
|
||||
</a>
|
||||
|
||||
### Sponsors
|
||||
|
||||
@@ -176,7 +179,6 @@ We'd also like to extend our sincere thanks the following sponsors who support o
|
||||
- [Tidelift](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) for offering support to both the maintainers and end-users through their [professional support](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) program
|
||||
- [Blackfire](https://www.blackfire.io/) for providing an Open-Source Profiler subscription
|
||||
- [JetBrains](https://www.jetbrains.com/) for supporting this project with complimentary [PhpStorm](https://www.jetbrains.com/phpstorm/) licenses
|
||||
- [Taylor Otwell](https://twitter.com/taylorotwell) for sponsoring this project through GitHub sponsors
|
||||
|
||||
Are you interested in sponsoring development of this project? See <https://www.colinodell.com/sponsor> for a list of ways to contribute.
|
||||
|
||||
|
||||
27
vendor/league/commonmark/composer.json
vendored
27
vendor/league/commonmark/composer.json
vendored
@@ -31,8 +31,8 @@
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"cebe/markdown": "^1.0",
|
||||
"commonmark/cmark": "0.30.0",
|
||||
"commonmark/commonmark.js": "0.30.0",
|
||||
"commonmark/cmark": "0.31.1",
|
||||
"commonmark/commonmark.js": "0.31.1",
|
||||
"composer/package-versions-deprecated": "^1.8",
|
||||
"embed/embed": "^4.4",
|
||||
"erusev/parsedown": "^1.0",
|
||||
@@ -40,10 +40,11 @@
|
||||
"michelf/php-markdown": "^1.4 || ^2.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpstan/phpstan": "^1.8.2",
|
||||
"phpunit/phpunit": "^9.5.21",
|
||||
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
|
||||
"scrutinizer/ocular": "^1.8.1",
|
||||
"symfony/finder": "^5.3 | ^6.0",
|
||||
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
|
||||
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
|
||||
"symfony/process": "^5.4 | ^6.0 | ^7.0",
|
||||
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
|
||||
"unleashedtech/php-coding-standard": "^3.1.1",
|
||||
"vimeo/psalm": "^4.24.0 || ^5.0.0"
|
||||
},
|
||||
@@ -56,9 +57,9 @@
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "commonmark/commonmark.js",
|
||||
"version": "0.30.0",
|
||||
"version": "0.31.1",
|
||||
"dist": {
|
||||
"url": "https://github.com/commonmark/commonmark.js/archive/0.30.0.zip",
|
||||
"url": "https://github.com/commonmark/commonmark.js/archive/0.31.1.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
@@ -67,9 +68,9 @@
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "commonmark/cmark",
|
||||
"version": "0.30.0",
|
||||
"version": "0.31.1",
|
||||
"dist": {
|
||||
"url": "https://github.com/commonmark/cmark/archive/0.30.0.zip",
|
||||
"url": "https://github.com/commonmark/cmark/archive/0.31.1.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
@@ -80,7 +81,7 @@
|
||||
"name": "github/gfm",
|
||||
"version": "0.29.0",
|
||||
"dist": {
|
||||
"url": "https://github.com/github/cmark-gfm/archive/0.29.0.gfm.9.zip",
|
||||
"url": "https://github.com/github/cmark-gfm/archive/0.29.0.gfm.13.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
@@ -103,16 +104,18 @@
|
||||
"phpstan": "phpstan analyse",
|
||||
"phpunit": "phpunit --no-coverage",
|
||||
"psalm": "psalm --stats",
|
||||
"pathological": "tests/pathological/test.php",
|
||||
"test": [
|
||||
"@phpcs",
|
||||
"@phpstan",
|
||||
"@psalm",
|
||||
"@phpunit"
|
||||
"@phpunit",
|
||||
"@pathological"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.5-dev"
|
||||
"dev-main": "2.8-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
83
vendor/league/commonmark/src/Delimiter/Bracket.php
vendored
Normal file
83
vendor/league/commonmark/src/Delimiter/Bracket.php
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Delimiter;
|
||||
|
||||
use League\CommonMark\Node\Node;
|
||||
|
||||
final class Bracket
|
||||
{
|
||||
private Node $node;
|
||||
private ?Bracket $previous;
|
||||
private bool $hasNext = false;
|
||||
private int $position;
|
||||
private bool $image;
|
||||
private bool $active = true;
|
||||
|
||||
public function __construct(Node $node, ?Bracket $previous, int $position, bool $image)
|
||||
{
|
||||
$this->node = $node;
|
||||
$this->previous = $previous;
|
||||
$this->position = $position;
|
||||
$this->image = $image;
|
||||
}
|
||||
|
||||
public function getNode(): Node
|
||||
{
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
public function getPrevious(): ?Bracket
|
||||
{
|
||||
return $this->previous;
|
||||
}
|
||||
|
||||
public function hasNext(): bool
|
||||
{
|
||||
return $this->hasNext;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function isImage(): bool
|
||||
{
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only valid in the context of non-images (links)
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setHasNext(bool $hasNext): void
|
||||
{
|
||||
$this->hasNext = $hasNext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function setActive(bool $active): void
|
||||
{
|
||||
$this->active = $active;
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,14 @@ interface DelimiterInterface
|
||||
|
||||
public function canOpen(): bool;
|
||||
|
||||
/**
|
||||
* @deprecated This method is no longer used internally and will be removed in 3.0
|
||||
*/
|
||||
public function isActive(): bool;
|
||||
|
||||
/**
|
||||
* @deprecated This method is no longer used internally and will be removed in 3.0
|
||||
*/
|
||||
public function setActive(bool $active): void;
|
||||
|
||||
public function getChar(): string;
|
||||
|
||||
@@ -62,16 +62,20 @@ final class DelimiterParser implements InlineParserInterface
|
||||
|
||||
[$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $processor);
|
||||
|
||||
if (! ($canOpen || $canClose)) {
|
||||
$inlineContext->getContainer()->appendChild(new Text(\str_repeat($character, $numDelims)));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$node = new Text(\str_repeat($character, $numDelims), [
|
||||
'delim' => true,
|
||||
]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack to this opener
|
||||
if ($canOpen || $canClose) {
|
||||
$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
|
||||
$inlineContext->getDelimiterStack()->push($delimiter);
|
||||
}
|
||||
$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose, $inlineContext->getCursor()->getPosition());
|
||||
$inlineContext->getDelimiterStack()->push($delimiter);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,16 +19,47 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Delimiter;
|
||||
|
||||
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
|
||||
use League\CommonMark\Node\Inline\AdjacentTextMerger;
|
||||
use League\CommonMark\Node\Node;
|
||||
|
||||
final class DelimiterStack
|
||||
{
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ?DelimiterInterface $top = null;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ?Bracket $brackets = null;
|
||||
|
||||
/**
|
||||
* @deprecated This property will be removed in 3.0 once all delimiters MUST have an index/position
|
||||
*
|
||||
* @var \SplObjectStorage<DelimiterInterface, int>|\WeakMap<DelimiterInterface, int>
|
||||
*/
|
||||
private $missingIndexCache;
|
||||
|
||||
|
||||
private int $remainingDelimiters = 0;
|
||||
|
||||
public function __construct(int $maximumStackSize = PHP_INT_MAX)
|
||||
{
|
||||
$this->remainingDelimiters = $maximumStackSize;
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
/** @psalm-suppress PropertyTypeCoercion */
|
||||
$this->missingIndexCache = new \WeakMap(); // @phpstan-ignore-line
|
||||
} else {
|
||||
$this->missingIndexCache = new \SplObjectStorage(); // @phpstan-ignore-line
|
||||
}
|
||||
}
|
||||
|
||||
public function push(DelimiterInterface $newDelimiter): void
|
||||
{
|
||||
if ($this->remainingDelimiters-- <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$newDelimiter->setPrevious($this->top);
|
||||
|
||||
if ($this->top !== null) {
|
||||
@@ -38,14 +69,54 @@ final class DelimiterStack
|
||||
$this->top = $newDelimiter;
|
||||
}
|
||||
|
||||
private function findEarliest(?DelimiterInterface $stackBottom = null): ?DelimiterInterface
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addBracket(Node $node, int $index, bool $image): void
|
||||
{
|
||||
$delimiter = $this->top;
|
||||
while ($delimiter !== null && $delimiter->getPrevious() !== $stackBottom) {
|
||||
$delimiter = $delimiter->getPrevious();
|
||||
if ($this->brackets !== null) {
|
||||
$this->brackets->setHasNext(true);
|
||||
}
|
||||
|
||||
return $delimiter;
|
||||
$this->brackets = new Bracket($node, $this->brackets, $index, $image);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
public function getLastBracket(): ?Bracket
|
||||
{
|
||||
return $this->brackets;
|
||||
}
|
||||
|
||||
private function findEarliest(int $stackBottom): ?DelimiterInterface
|
||||
{
|
||||
// Move back to first relevant delim.
|
||||
$delimiter = $this->top;
|
||||
$lastChecked = null;
|
||||
|
||||
while ($delimiter !== null && self::getIndex($delimiter) > $stackBottom) {
|
||||
$lastChecked = $delimiter;
|
||||
$delimiter = $delimiter->getPrevious();
|
||||
}
|
||||
|
||||
return $lastChecked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function removeBracket(): void
|
||||
{
|
||||
if ($this->brackets === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->brackets = $this->brackets->getPrevious();
|
||||
|
||||
if ($this->brackets !== null) {
|
||||
$this->brackets->setHasNext(false);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeDelimiter(DelimiterInterface $delimiter): void
|
||||
@@ -62,6 +133,19 @@ final class DelimiterStack
|
||||
/** @psalm-suppress PossiblyNullReference */
|
||||
$delimiter->getNext()->setPrevious($delimiter->getPrevious());
|
||||
}
|
||||
|
||||
// Nullify all references from the removed delimiter to other delimiters.
|
||||
// All references to this particular delimiter in the linked list should be gone,
|
||||
// but it's possible we're still hanging on to other references to things that
|
||||
// have been (or soon will be) removed, which may interfere with efficient
|
||||
// garbage collection by the PHP runtime.
|
||||
// Explicitly releasing these references should help to avoid possible
|
||||
// segfaults like in https://bugs.php.net/bug.php?id=68606.
|
||||
$delimiter->setPrevious(null);
|
||||
$delimiter->setNext(null);
|
||||
|
||||
// TODO: Remove the line below once PHP 7.4 support is dropped, as WeakMap won't hold onto the reference, making this unnecessary
|
||||
unset($this->missingIndexCache[$delimiter]);
|
||||
}
|
||||
|
||||
private function removeDelimiterAndNode(DelimiterInterface $delimiter): void
|
||||
@@ -72,21 +156,30 @@ final class DelimiterStack
|
||||
|
||||
private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
|
||||
{
|
||||
$delimiter = $closer->getPrevious();
|
||||
while ($delimiter !== null && $delimiter !== $opener) {
|
||||
$delimiter = $closer->getPrevious();
|
||||
$openerPosition = self::getIndex($opener);
|
||||
while ($delimiter !== null && self::getIndex($delimiter) > $openerPosition) {
|
||||
$previous = $delimiter->getPrevious();
|
||||
$this->removeDelimiter($delimiter);
|
||||
$delimiter = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAll(?DelimiterInterface $stackBottom = null): void
|
||||
/**
|
||||
* @param DelimiterInterface|int|null $stackBottom
|
||||
*/
|
||||
public function removeAll($stackBottom = null): void
|
||||
{
|
||||
while ($this->top && $this->top !== $stackBottom) {
|
||||
$stackBottomPosition = \is_int($stackBottom) ? $stackBottom : self::getIndex($stackBottom);
|
||||
|
||||
while ($this->top && $this->getIndex($this->top) > $stackBottomPosition) {
|
||||
$this->removeDelimiter($this->top);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is no longer used internally and will be removed in 3.0
|
||||
*/
|
||||
public function removeEarlierMatches(string $character): void
|
||||
{
|
||||
$opener = $this->top;
|
||||
@@ -100,6 +193,20 @@ final class DelimiterStack
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function deactivateLinkOpeners(): void
|
||||
{
|
||||
$opener = $this->brackets;
|
||||
while ($opener !== null && $opener->isActive()) {
|
||||
$opener->setActive(false);
|
||||
$opener = $opener->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method is no longer used internally and will be removed in 3.0
|
||||
*
|
||||
* @param string|string[] $characters
|
||||
*/
|
||||
public function searchByCharacter($characters): ?DelimiterInterface
|
||||
@@ -120,30 +227,44 @@ final class DelimiterStack
|
||||
return $opener;
|
||||
}
|
||||
|
||||
public function processDelimiters(?DelimiterInterface $stackBottom, DelimiterProcessorCollection $processors): void
|
||||
/**
|
||||
* @param DelimiterInterface|int|null $stackBottom
|
||||
*
|
||||
* @todo change $stackBottom to an int in 3.0
|
||||
*/
|
||||
public function processDelimiters($stackBottom, DelimiterProcessorCollection $processors): void
|
||||
{
|
||||
/** @var array<string, int> $openersBottom */
|
||||
$openersBottom = [];
|
||||
|
||||
$stackBottomPosition = \is_int($stackBottom) ? $stackBottom : self::getIndex($stackBottom);
|
||||
|
||||
// Find first closer above stackBottom
|
||||
$closer = $this->findEarliest($stackBottom);
|
||||
$closer = $this->findEarliest($stackBottomPosition);
|
||||
|
||||
// Move forward, looking for closers, and handling each
|
||||
while ($closer !== null) {
|
||||
$delimiterChar = $closer->getChar();
|
||||
$closingDelimiterChar = $closer->getChar();
|
||||
|
||||
$delimiterProcessor = $processors->getDelimiterProcessor($delimiterChar);
|
||||
$delimiterProcessor = $processors->getDelimiterProcessor($closingDelimiterChar);
|
||||
if (! $closer->canClose() || $delimiterProcessor === null) {
|
||||
$closer = $closer->getNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($delimiterProcessor instanceof CacheableDelimiterProcessorInterface) {
|
||||
$openersBottomCacheKey = $delimiterProcessor->getCacheKey($closer);
|
||||
} else {
|
||||
$openersBottomCacheKey = $closingDelimiterChar;
|
||||
}
|
||||
|
||||
$openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
|
||||
|
||||
$useDelims = 0;
|
||||
$openerFound = false;
|
||||
$potentialOpenerFound = false;
|
||||
$opener = $closer->getPrevious();
|
||||
while ($opener !== null && $opener !== $stackBottom && $opener !== ($openersBottom[$delimiterChar] ?? null)) {
|
||||
while ($opener !== null && ($openerPosition = self::getIndex($opener)) > $stackBottomPosition && $openerPosition >= ($openersBottom[$openersBottomCacheKey] ?? 0)) {
|
||||
if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
|
||||
$potentialOpenerFound = true;
|
||||
$useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
|
||||
@@ -157,23 +278,22 @@ final class DelimiterStack
|
||||
}
|
||||
|
||||
if (! $openerFound) {
|
||||
if (! $potentialOpenerFound) {
|
||||
// Only do this when we didn't even have a potential
|
||||
// opener (one that matches the character and can open).
|
||||
// If an opener was rejected because of the number of
|
||||
// delimiters (e.g. because of the "multiple of 3"
|
||||
// Set lower bound for future searches for openersrule),
|
||||
// we want to consider it next time because the number
|
||||
// of delimiters can change as we continue processing.
|
||||
$openersBottom[$delimiterChar] = $closer->getPrevious();
|
||||
if (! $closer->canOpen()) {
|
||||
// We can remove a closer that can't be an opener,
|
||||
// once we've seen there's no matching opener.
|
||||
$this->removeDelimiter($closer);
|
||||
}
|
||||
// Set lower bound for future searches
|
||||
// TODO: Remove this conditional check in 3.0. It only exists to prevent behavioral BC breaks in 2.x.
|
||||
if ($potentialOpenerFound === false || $delimiterProcessor instanceof CacheableDelimiterProcessorInterface) {
|
||||
$openersBottom[$openersBottomCacheKey] = self::getIndex($closer);
|
||||
}
|
||||
|
||||
if (! $potentialOpenerFound && ! $closer->canOpen()) {
|
||||
// We can remove a closer that can't be an opener,
|
||||
// once we've seen there's no matching opener.
|
||||
$next = $closer->getNext();
|
||||
$this->removeDelimiter($closer);
|
||||
$closer = $next;
|
||||
} else {
|
||||
$closer = $closer->getNext();
|
||||
}
|
||||
|
||||
$closer = $closer->getNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -209,6 +329,68 @@ final class DelimiterStack
|
||||
}
|
||||
|
||||
// Remove all delimiters
|
||||
$this->removeAll($stackBottom);
|
||||
$this->removeAll($stackBottomPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
while ($this->top) {
|
||||
$this->removeDelimiter($this->top);
|
||||
}
|
||||
|
||||
while ($this->brackets) {
|
||||
$this->removeBracket();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be dropped in 3.0 once all delimiters MUST have an index/position
|
||||
*/
|
||||
private function getIndex(?DelimiterInterface $delimiter): int
|
||||
{
|
||||
if ($delimiter === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (($index = $delimiter->getIndex()) !== null) {
|
||||
return $index;
|
||||
}
|
||||
|
||||
if (isset($this->missingIndexCache[$delimiter])) {
|
||||
return $this->missingIndexCache[$delimiter];
|
||||
}
|
||||
|
||||
$prev = $delimiter->getPrevious();
|
||||
$next = $delimiter->getNext();
|
||||
|
||||
$i = 0;
|
||||
do {
|
||||
$i++;
|
||||
if ($prev === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($prev->getIndex() !== null) {
|
||||
return $this->missingIndexCache[$delimiter] = $prev->getIndex() + $i;
|
||||
}
|
||||
} while ($prev = $prev->getPrevious());
|
||||
|
||||
$j = 0;
|
||||
do {
|
||||
$j++;
|
||||
if ($next === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($next->getIndex() !== null) {
|
||||
return $this->missingIndexCache[$delimiter] = $next->getIndex() - $j;
|
||||
}
|
||||
} while ($next = $next->getNext());
|
||||
|
||||
// No index was defined on this delimiter, and none could be guesstimated based on the stack.
|
||||
return $this->missingIndexCache[$delimiter] = $this->getIndex($delimiter->getPrevious()) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
46
vendor/league/commonmark/src/Delimiter/Processor/CacheableDelimiterProcessorInterface.php
vendored
Normal file
46
vendor/league/commonmark/src/Delimiter/Processor/CacheableDelimiterProcessorInterface.php
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Delimiter\Processor;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
|
||||
/**
|
||||
* Special marker interface for delimiter processors that return dynamic values from getDelimiterUse()
|
||||
*
|
||||
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
|
||||
* cache the lower bound when searching for a matching opener. This gets complicated for delimiter processors
|
||||
* that use a dynamic number of characters (like with emphasis and its "multiple of 3" rule).
|
||||
*/
|
||||
interface CacheableDelimiterProcessorInterface extends DelimiterProcessorInterface
|
||||
{
|
||||
/**
|
||||
* Returns a cache key of the factors that determine the number of characters to use.
|
||||
*
|
||||
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
|
||||
* cache the lower bound when searching for a matching opener. This lower bound is usually quite simple;
|
||||
* for example, with quotes, it's just the last opener with that characted. However, this gets complicated
|
||||
* for delimiter processors that use a dynamic number of characters (like with emphasis and its "multiple
|
||||
* of 3" rule), because the delimiter length being considered may change during processing because of that
|
||||
* dynamic logic in getDelimiterUse(). Therefore, we cannot safely cache the lower bound for these dynamic
|
||||
* processors without knowing the factors that determine the number of characters to use.
|
||||
*
|
||||
* At a minimum, this should include the delimiter character, plus any other factors used to determine
|
||||
* the result of getDelimiterUse(). The format of the string is not important so long as it is unique
|
||||
* (compared to other processors) and consistent for a given set of factors.
|
||||
*
|
||||
* If getDelimiterUse() always returns the same hard-coded value, this method should return just
|
||||
* the delimiter character.
|
||||
*/
|
||||
public function getCacheKey(DelimiterInterface $closer): string;
|
||||
}
|
||||
@@ -58,6 +58,9 @@ interface DelimiterProcessorInterface
|
||||
* return 0 when it doesn't want to allow this particular combination of
|
||||
* delimiter runs.
|
||||
*
|
||||
* IMPORTANT: Unless this method returns the same hard-coded value in all cases,
|
||||
* you MUST implement the CacheableDelimiterProcessorInterface interface instead.
|
||||
*
|
||||
* @param DelimiterInterface $opener The opening delimiter run
|
||||
* @param DelimiterInterface $closer The closing delimiter run
|
||||
*/
|
||||
|
||||
@@ -432,6 +432,7 @@ final class Environment implements EnvironmentInterface, EnvironmentBuilderInter
|
||||
'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
|
||||
'allow_unsafe_links' => Expect::bool(true),
|
||||
'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
|
||||
'max_delimiters_per_line' => Expect::type('int')->default(PHP_INT_MAX),
|
||||
'renderer' => Expect::structure([
|
||||
'block_separator' => Expect::string("\n"),
|
||||
'inner_separator' => Expect::string("\n"),
|
||||
|
||||
@@ -19,14 +19,26 @@ use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
|
||||
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockStartParser;
|
||||
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class AttributesExtension implements ExtensionInterface
|
||||
final class AttributesExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('attributes', Expect::structure([
|
||||
'allow' => Expect::arrayOf('string')->default([]),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$allowList = $environment->getConfiguration()->get('attributes.allow');
|
||||
$allowUnsafeLinks = $environment->getConfiguration()->get('allow_unsafe_links');
|
||||
|
||||
$environment->addBlockStartParser(new AttributesBlockStartParser());
|
||||
$environment->addInlineParser(new AttributesInlineParser());
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener($allowList, $allowUnsafeLinks), 'processDocument']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,19 @@ final class AttributesListener
|
||||
private const DIRECTION_PREFIX = 'prefix';
|
||||
private const DIRECTION_SUFFIX = 'suffix';
|
||||
|
||||
/** @var list<string> */
|
||||
private array $allowList;
|
||||
private bool $allowUnsafeLinks;
|
||||
|
||||
/**
|
||||
* @param list<string> $allowList
|
||||
*/
|
||||
public function __construct(array $allowList = [], bool $allowUnsafeLinks = true)
|
||||
{
|
||||
$this->allowList = $allowList;
|
||||
$this->allowUnsafeLinks = $allowUnsafeLinks;
|
||||
}
|
||||
|
||||
public function processDocument(DocumentParsedEvent $event): void
|
||||
{
|
||||
foreach ($event->getDocument()->iterator() as $node) {
|
||||
@@ -50,7 +63,7 @@ final class AttributesListener
|
||||
$attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
|
||||
}
|
||||
|
||||
$target->data->set('attributes', $attributes);
|
||||
$target->data->set('attributes', AttributesHelper::filterAttributes($attributes, $this->allowList, $this->allowUnsafeLinks));
|
||||
}
|
||||
|
||||
$node->detach();
|
||||
|
||||
@@ -23,7 +23,7 @@ use League\CommonMark\Util\RegexHelper;
|
||||
*/
|
||||
final class AttributesHelper
|
||||
{
|
||||
private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
|
||||
private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s.}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
|
||||
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i';
|
||||
|
||||
/**
|
||||
@@ -75,6 +75,11 @@ final class AttributesHelper
|
||||
/** @psalm-suppress PossiblyUndefinedArrayOffset */
|
||||
[$name, $value] = \explode('=', $attribute, 2);
|
||||
|
||||
if ($value === 'true') {
|
||||
$attributes[$name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$first = $value[0];
|
||||
$last = \substr($value, -1);
|
||||
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) {
|
||||
@@ -134,4 +139,42 @@ final class AttributesHelper
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
* @param list<string> $allowList
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function filterAttributes(array $attributes, array $allowList, bool $allowUnsafeLinks): array
|
||||
{
|
||||
$allowList = \array_fill_keys($allowList, true);
|
||||
|
||||
foreach ($attributes as $name => $value) {
|
||||
$attrNameLower = \strtolower($name);
|
||||
|
||||
// Remove any unsafe links
|
||||
if (! $allowUnsafeLinks && ($attrNameLower === 'href' || $attrNameLower === 'src') && \is_string($value) && RegexHelper::isLinkPotentiallyUnsafe($value)) {
|
||||
unset($attributes[$name]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// No allowlist?
|
||||
if ($allowList === []) {
|
||||
// Just remove JS event handlers
|
||||
if (\str_starts_with($attrNameLower, 'on')) {
|
||||
unset($attributes[$name]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove any attributes not in that allowlist (case-sensitive)
|
||||
if (! isset($allowList[$name])) {
|
||||
unset($attributes[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,26 @@ declare(strict_types=1);
|
||||
namespace League\CommonMark\Extension\Autolink;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class AutolinkExtension implements ExtensionInterface
|
||||
final class AutolinkExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('autolink', Expect::structure([
|
||||
'allowed_protocols' => Expect::listOf('string')->default(['http', 'https', 'ftp'])->mergeDefaults(false),
|
||||
'default_protocol' => Expect::string()->default('http'),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$environment->addInlineParser(new EmailAutolinkParser());
|
||||
$environment->addInlineParser(new UrlAutolinkParser());
|
||||
$environment->addInlineParser(new UrlAutolinkParser(
|
||||
$environment->getConfiguration()->get('autolink.allowed_protocols'),
|
||||
$environment->getConfiguration()->get('autolink.default_protocol'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ final class UrlAutolinkParser implements InlineParserInterface
|
||||
(?:
|
||||
(?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode
|
||||
|
|
||||
(?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name
|
||||
(?:[\pL\pN\pS\pM\-\_]++\.){1,127}[\pL\pN\pM]++ # a multi-level domain name; total length must be 253 bytes or less
|
||||
|
|
||||
[a-z0-9\-\_]++ # a single-level domain name
|
||||
)\.?
|
||||
@@ -56,7 +56,7 @@ final class UrlAutolinkParser implements InlineParserInterface
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private array $prefixes = ['www'];
|
||||
private array $prefixes = ['www.'];
|
||||
|
||||
/**
|
||||
* @psalm-var non-empty-string
|
||||
@@ -65,10 +65,12 @@ final class UrlAutolinkParser implements InlineParserInterface
|
||||
*/
|
||||
private string $finalRegex;
|
||||
|
||||
private string $defaultProtocol;
|
||||
|
||||
/**
|
||||
* @param array<int, string> $allowedProtocols
|
||||
*/
|
||||
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'])
|
||||
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'], string $defaultProtocol = 'http')
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress PropertyTypeCoercion
|
||||
@@ -78,6 +80,8 @@ final class UrlAutolinkParser implements InlineParserInterface
|
||||
foreach ($allowedProtocols as $protocol) {
|
||||
$this->prefixes[] = $protocol . '://';
|
||||
}
|
||||
|
||||
$this->defaultProtocol = $defaultProtocol;
|
||||
}
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
@@ -120,9 +124,9 @@ final class UrlAutolinkParser implements InlineParserInterface
|
||||
|
||||
$cursor->advanceBy(\mb_strlen($url, 'UTF-8'));
|
||||
|
||||
// Auto-prefix 'http://' onto 'www' URLs
|
||||
// Auto-prefix 'http(s)://' onto 'www' URLs
|
||||
if (\substr($url, 0, 4) === 'www.') {
|
||||
$inlineContext->getContainer()->appendChild(new Link('http://' . $url, $url));
|
||||
$inlineContext->getContainer()->appendChild(new Link($this->defaultProtocol . '://' . $url, $url));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ declare(strict_types=1);
|
||||
namespace League\CommonMark\Extension\CommonMark\Delimiter\Processor;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, ConfigurationAwareInterface
|
||||
final class EmphasisDelimiterProcessor implements CacheableDelimiterProcessorInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private string $char;
|
||||
@@ -105,4 +105,15 @@ final class EmphasisDelimiterProcessor implements DelimiterProcessorInterface, C
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function getCacheKey(DelimiterInterface $closer): string
|
||||
{
|
||||
return \sprintf(
|
||||
'%s-%s-%d-%d',
|
||||
$this->char,
|
||||
$closer->canOpen() ? 'canOpen' : 'cannotOpen',
|
||||
$closer->getOriginalLength() % 3,
|
||||
$closer->getLength(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class ListBlock extends AbstractBlock implements TightBlockInterface
|
||||
public const DELIM_PERIOD = 'period';
|
||||
public const DELIM_PAREN = 'paren';
|
||||
|
||||
protected bool $tight = false;
|
||||
protected bool $tight = false; // TODO Make lists tight by default in v3
|
||||
|
||||
/** @psalm-readonly */
|
||||
protected ListData $listData;
|
||||
|
||||
@@ -44,7 +44,7 @@ final class FencedCodeParser extends AbstractBlockContinueParser
|
||||
{
|
||||
// Check for closing code fence
|
||||
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) {
|
||||
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?= *$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
|
||||
$match = RegexHelper::matchFirst('/^(?:`{3,}|~{3,})(?=[ \t]*$)/', $cursor->getLine(), $cursor->getNextNonSpacePosition());
|
||||
if ($match !== null && \strlen($match[0]) >= $this->block->getLength()) {
|
||||
// closing fence - we're at end of line, so we can finalize now
|
||||
return BlockContinue::finished();
|
||||
|
||||
@@ -63,21 +63,14 @@ final class IndentedCodeParser extends AbstractBlockContinueParser
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
$reversed = \array_reverse($this->strings->toArray(), true);
|
||||
foreach ($reversed as $index => $line) {
|
||||
if ($line !== '' && $line !== "\n" && ! \preg_match('/^(\n *)$/', $line)) {
|
||||
break;
|
||||
}
|
||||
$lines = $this->strings->toArray();
|
||||
|
||||
unset($reversed[$index]);
|
||||
// Note that indented code block cannot be empty, so $lines will always have at least one non-empty element
|
||||
while (\preg_match('/^[ \t]*$/', \end($lines))) { // @phpstan-ignore-line
|
||||
\array_pop($lines);
|
||||
}
|
||||
|
||||
$fixed = \array_reverse($reversed);
|
||||
$tmp = \implode("\n", $fixed);
|
||||
if (\substr($tmp, -1) !== "\n") {
|
||||
$tmp .= "\n";
|
||||
}
|
||||
|
||||
$this->block->setLiteral($tmp);
|
||||
$this->block->setLiteral(\implode("\n", $lines) . "\n");
|
||||
$this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,6 @@ final class ListBlockParser extends AbstractBlockContinueParser
|
||||
/** @psalm-readonly */
|
||||
private ListBlock $block;
|
||||
|
||||
private bool $hadBlankLine = false;
|
||||
|
||||
private int $linesAfterBlank = 0;
|
||||
|
||||
public function __construct(ListData $listData)
|
||||
{
|
||||
$this->block = new ListBlock($listData);
|
||||
@@ -48,32 +44,50 @@ final class ListBlockParser extends AbstractBlockContinueParser
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
if (! $childBlock instanceof ListItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Another list item is being added to this list block.
|
||||
// If the previous line was blank, that means this list
|
||||
// block is "loose" (not tight).
|
||||
if ($this->hadBlankLine && $this->linesAfterBlank === 1) {
|
||||
$this->block->setTight(false);
|
||||
$this->hadBlankLine = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $childBlock instanceof ListItem;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if ($cursor->isBlank()) {
|
||||
$this->hadBlankLine = true;
|
||||
$this->linesAfterBlank = 0;
|
||||
} elseif ($this->hadBlankLine) {
|
||||
$this->linesAfterBlank++;
|
||||
}
|
||||
|
||||
// List blocks themselves don't have any markers, only list items. So try to stay in the list.
|
||||
// If there is a block start other than list item, canContain makes sure that this list is closed.
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
$item = $this->block->firstChild();
|
||||
while ($item instanceof AbstractBlock) {
|
||||
// check for non-final list item ending with blank line:
|
||||
if ($item->next() !== null && self::endsWithBlankLine($item)) {
|
||||
$this->block->setTight(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// recurse into children of list item, to see if there are spaces between any of them
|
||||
$subitem = $item->firstChild();
|
||||
while ($subitem instanceof AbstractBlock) {
|
||||
if ($subitem->next() && self::endsWithBlankLine($subitem)) {
|
||||
$this->block->setTight(false);
|
||||
break 2;
|
||||
}
|
||||
|
||||
$subitem = $subitem->next();
|
||||
}
|
||||
|
||||
$item = $item->next();
|
||||
}
|
||||
|
||||
$lastChild = $this->block->lastChild();
|
||||
if ($lastChild instanceof AbstractBlock) {
|
||||
$this->block->setEndLine($lastChild->getEndLine());
|
||||
}
|
||||
}
|
||||
|
||||
private static function endsWithBlankLine(AbstractBlock $block): bool
|
||||
{
|
||||
$next = $block->next();
|
||||
|
||||
return $next instanceof AbstractBlock && $block->getEndLine() !== $next->getStartLine() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ final class ListBlockStartParser implements BlockStartParserInterface, Configura
|
||||
if (! ($matched instanceof ListBlockParser) || ! $listData->equals($matched->getBlock()->getListData())) {
|
||||
$listBlockParser = new ListBlockParser($listData);
|
||||
// We start out with assuming a list is tight. If we find a blank line, we set it to loose later.
|
||||
// TODO for 3.0: Just make them tight by default in the block so we can remove this call
|
||||
$listBlockParser->getBlock()->setTight(true);
|
||||
|
||||
return BlockStart::of($listBlockParser, $listItemParser)->at($cursor);
|
||||
|
||||
@@ -13,11 +13,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
@@ -28,8 +26,6 @@ final class ListItemParser extends AbstractBlockContinueParser
|
||||
/** @psalm-readonly */
|
||||
private ListItem $block;
|
||||
|
||||
private bool $hadBlankLine = false;
|
||||
|
||||
public function __construct(ListData $listData)
|
||||
{
|
||||
$this->block = new ListItem($listData);
|
||||
@@ -47,18 +43,7 @@ final class ListItemParser extends AbstractBlockContinueParser
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
if ($this->hadBlankLine) {
|
||||
// We saw a blank line in this list item, that means the list block is loose.
|
||||
//
|
||||
// spec: if any of its constituent list items directly contain two block-level elements with a blank line
|
||||
// between them
|
||||
$parent = $this->block->parent();
|
||||
if ($parent instanceof ListBlock) {
|
||||
$parent->setTight(false);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return ! $childBlock instanceof ListItem;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
@@ -69,9 +54,6 @@ final class ListItemParser extends AbstractBlockContinueParser
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
$activeBlock = $activeBlockParser->getBlock();
|
||||
// If the active block is a code block, blank lines in it should not affect if the list is tight.
|
||||
$this->hadBlankLine = $activeBlock instanceof Paragraph || $activeBlock instanceof ListItem;
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
@@ -87,4 +69,14 @@ final class ListItemParser extends AbstractBlockContinueParser
|
||||
// Note: We'll hit this case for lazy continuation lines, they will get added later.
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
if (($lastChild = $this->block->lastChild()) instanceof AbstractBlock) {
|
||||
$this->block->setEndLine($lastChild->getEndLine());
|
||||
} else {
|
||||
// Empty list item
|
||||
$this->block->setEndLine($this->block->getStartLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,27 @@ namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class BacktickParser implements InlineParserInterface
|
||||
{
|
||||
/**
|
||||
* Max bound for backtick code span delimiters.
|
||||
*
|
||||
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
|
||||
*/
|
||||
private const MAX_BACKTICKS = 1000;
|
||||
|
||||
/** @var \WeakReference<Cursor>|null */
|
||||
private ?\WeakReference $lastCursor = null;
|
||||
private bool $lastCursorScanned = false;
|
||||
|
||||
/** @var array<int, int> backtick count => position of known ender */
|
||||
private array $seenBackticks = [];
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::regex('`+');
|
||||
@@ -38,11 +53,7 @@ final class BacktickParser implements InlineParserInterface
|
||||
$currentPosition = $cursor->getPosition();
|
||||
$previousState = $cursor->saveState();
|
||||
|
||||
while ($matchingTicks = $cursor->match('/`+/m')) {
|
||||
if ($matchingTicks !== $ticks) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
|
||||
$code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
|
||||
|
||||
$c = \preg_replace('/\n/m', ' ', $code) ?? '';
|
||||
@@ -67,4 +78,55 @@ final class BacktickParser implements InlineParserInterface
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the matching closer for a backtick code span.
|
||||
*
|
||||
* Leverages some caching to avoid traversing the same cursor multiple times when
|
||||
* we've already seen all the potential backtick closers.
|
||||
*
|
||||
* @see https://github.com/commonmark/cmark/commit/8ed5c9d
|
||||
*
|
||||
* @param int $openTickLength Number of backticks in the opening sequence
|
||||
* @param Cursor $cursor Cursor to scan
|
||||
*
|
||||
* @return bool True if a matching closer was found, false otherwise
|
||||
*/
|
||||
private function findMatchingTicks(int $openTickLength, Cursor $cursor): bool
|
||||
{
|
||||
// Reset the seenBackticks cache if this is a new cursor
|
||||
if ($this->lastCursor === null || $this->lastCursor->get() !== $cursor) {
|
||||
$this->seenBackticks = [];
|
||||
$this->lastCursor = \WeakReference::create($cursor);
|
||||
$this->lastCursorScanned = false;
|
||||
}
|
||||
|
||||
if ($openTickLength > self::MAX_BACKTICKS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return if we already know there's no closer
|
||||
if ($this->lastCursorScanned && isset($this->seenBackticks[$openTickLength]) && $this->seenBackticks[$openTickLength] <= $cursor->getPosition()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while ($ticks = $cursor->match('/`{1,' . self::MAX_BACKTICKS . '}/m')) {
|
||||
$numTicks = \strlen($ticks);
|
||||
|
||||
// Did we find the closer?
|
||||
if ($numTicks === $openTickLength) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Store position of closer
|
||||
if ($numTicks <= self::MAX_BACKTICKS) {
|
||||
$this->seenBackticks[$numTicks] = $cursor->getPosition() - $numTicks;
|
||||
}
|
||||
}
|
||||
|
||||
// Got through whole input without finding closer
|
||||
$this->lastCursorScanned = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Delimiter\Delimiter;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
@@ -38,8 +37,7 @@ final class BangParser implements InlineParserInterface
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack for this opener
|
||||
$delimiter = new Delimiter('!', 1, $node, true, false, $cursor->getPosition());
|
||||
$inlineContext->getDelimiterStack()->push($delimiter);
|
||||
$inlineContext->getDelimiterStack()->addBracket($node, $cursor->getPosition(), true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Delimiter\Bracket;
|
||||
use League\CommonMark\Environment\EnvironmentAwareInterface;
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\AbstractWebResource;
|
||||
@@ -46,14 +47,14 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
// Look through stack of delimiters for a [ or !
|
||||
$opener = $inlineContext->getDelimiterStack()->searchByCharacter(['[', '!']);
|
||||
$opener = $inlineContext->getDelimiterStack()->getLastBracket();
|
||||
if ($opener === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $opener->isActive()) {
|
||||
// no matched opener; remove from emphasis stack
|
||||
$inlineContext->getDelimiterStack()->removeDelimiter($opener);
|
||||
if (! $opener->isImage() && ! $opener->isActive()) {
|
||||
// no matched opener; remove from stack
|
||||
$inlineContext->getDelimiterStack()->removeBracket();
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -70,21 +71,19 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
|
||||
// Inline link?
|
||||
if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
|
||||
$link = $result;
|
||||
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener->getIndex(), $startPos)) {
|
||||
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener, $startPos)) {
|
||||
$reference = $link;
|
||||
$link = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
|
||||
} else {
|
||||
// No match
|
||||
$inlineContext->getDelimiterStack()->removeDelimiter($opener); // Remove this opener from stack
|
||||
// No match; remove this opener from stack
|
||||
$inlineContext->getDelimiterStack()->removeBracket();
|
||||
$cursor->restoreState($previousState);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$isImage = $opener->getChar() === '!';
|
||||
|
||||
$inline = $this->createInline($link['url'], $link['title'], $isImage, $reference ?? null);
|
||||
$opener->getInlineNode()->replaceWith($inline);
|
||||
$inline = $this->createInline($link['url'], $link['title'], $opener->isImage(), $reference ?? null);
|
||||
$opener->getNode()->replaceWith($inline);
|
||||
while (($label = $inline->next()) !== null) {
|
||||
// Is there a Mention or Link contained within this link?
|
||||
// CommonMark does not allow nested links, so we'll restore the original text.
|
||||
@@ -104,8 +103,9 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
|
||||
|
||||
// Process delimiters such as emphasis inside link/image
|
||||
$delimiterStack = $inlineContext->getDelimiterStack();
|
||||
$stackBottom = $opener->getPrevious();
|
||||
$stackBottom = $opener->getPosition();
|
||||
$delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
|
||||
$delimiterStack->removeBracket();
|
||||
$delimiterStack->removeAll($stackBottom);
|
||||
|
||||
// Merge any adjacent Text nodes together
|
||||
@@ -113,8 +113,8 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
|
||||
|
||||
// processEmphasis will remove this and later delimiters.
|
||||
// Now, for a link, we also remove earlier link openers (no links in links)
|
||||
if (! $isImage) {
|
||||
$inlineContext->getDelimiterStack()->removeEarlierMatches('[');
|
||||
if (! $opener->isImage()) {
|
||||
$inlineContext->getDelimiterStack()->deactivateLinkOpeners();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -168,21 +168,23 @@ final class CloseBracketParser implements InlineParserInterface, EnvironmentAwar
|
||||
return ['url' => $dest, 'title' => $title];
|
||||
}
|
||||
|
||||
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, ?int $openerIndex, int $startPos): ?ReferenceInterface
|
||||
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, Bracket $opener, int $startPos): ?ReferenceInterface
|
||||
{
|
||||
if ($openerIndex === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$savePos = $cursor->saveState();
|
||||
$beforeLabel = $cursor->getPosition();
|
||||
$n = LinkParserHelper::parseLinkLabel($cursor);
|
||||
if ($n === 0 || $n === 2) {
|
||||
$start = $openerIndex;
|
||||
$length = $startPos - $openerIndex;
|
||||
} else {
|
||||
if ($n > 2) {
|
||||
$start = $beforeLabel + 1;
|
||||
$length = $n - 2;
|
||||
} elseif (! $opener->hasNext()) {
|
||||
// Empty or missing second label means to use the first label as the reference.
|
||||
// The reference must not contain a bracket. If we know there's a bracket, we don't even bother checking it.
|
||||
$start = $opener->getPosition();
|
||||
$length = $startPos - $start;
|
||||
} else {
|
||||
$cursor->restoreState($savePos);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$referenceLabel = $cursor->getSubstring($start, $length);
|
||||
|
||||
@@ -16,7 +16,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Delimiter\Delimiter;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
@@ -36,8 +35,7 @@ final class OpenBracketParser implements InlineParserInterface
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack for this opener
|
||||
$delimiter = new Delimiter('[', 1, $node, true, false, $inlineContext->getCursor()->getPosition());
|
||||
$inlineContext->getDelimiterStack()->push($delimiter);
|
||||
$inlineContext->getDelimiterStack()->addBracket($node, $inlineContext->getCursor()->getPosition(), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,12 @@ final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRenderer
|
||||
|
||||
$infoWords = $node->getInfoWords();
|
||||
if (\count($infoWords) !== 0 && $infoWords[0] !== '') {
|
||||
$attrs->append('class', 'language-' . $infoWords[0]);
|
||||
$class = $infoWords[0];
|
||||
if (! \str_starts_with($class, 'language-')) {
|
||||
$class = 'language-' . $class;
|
||||
}
|
||||
|
||||
$attrs->append('class', $class);
|
||||
}
|
||||
|
||||
return new HtmlElement(
|
||||
|
||||
@@ -17,8 +17,9 @@ declare(strict_types=1);
|
||||
namespace League\CommonMark\Extension\CommonMark\Renderer\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Node\Block\TightBlockInterface;
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Renderer\ChildNodeRendererInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
@@ -39,11 +40,14 @@ final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererIn
|
||||
ListItem::assertInstanceOf($node);
|
||||
|
||||
$contents = $childRenderer->renderNodes($node->children());
|
||||
if (\substr($contents, 0, 1) === '<' && ! $this->startsTaskListItem($node)) {
|
||||
|
||||
$inTightList = ($parent = $node->parent()) && $parent instanceof TightBlockInterface && $parent->isTight();
|
||||
|
||||
if ($this->needsBlockSeparator($node->firstChild(), $inTightList)) {
|
||||
$contents = "\n" . $contents;
|
||||
}
|
||||
|
||||
if (\substr($contents, -1, 1) === '>') {
|
||||
if ($this->needsBlockSeparator($node->lastChild(), $inTightList)) {
|
||||
$contents .= "\n";
|
||||
}
|
||||
|
||||
@@ -65,10 +69,12 @@ final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererIn
|
||||
return [];
|
||||
}
|
||||
|
||||
private function startsTaskListItem(ListItem $block): bool
|
||||
private function needsBlockSeparator(?Node $child, bool $inTightList): bool
|
||||
{
|
||||
$firstChild = $block->firstChild();
|
||||
if ($child instanceof Paragraph && $inTightList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $firstChild instanceof Paragraph && $firstChild->firstChild() instanceof TaskListItemMarker;
|
||||
return $child instanceof AbstractBlock;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,19 @@ use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
final class QuoteParser implements InlineParserInterface
|
||||
{
|
||||
/**
|
||||
* @deprecated This constant is no longer used and will be removed in a future major release
|
||||
*/
|
||||
public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER];
|
||||
|
||||
/**
|
||||
* @deprecated This constant is no longer used and will be removed in a future major release
|
||||
*/
|
||||
public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER];
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::oneOf(...\array_merge(self::DOUBLE_QUOTES, self::SINGLE_QUOTES));
|
||||
return InlineParserMatch::oneOf(Quote::SINGLE_QUOTE, Quote::DOUBLE_QUOTE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,8 +46,7 @@ final class QuoteParser implements InlineParserInterface
|
||||
{
|
||||
$char = $inlineContext->getFullMatch();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
|
||||
$normalizedCharacter = $this->getNormalizedQuoteCharacter($char);
|
||||
$index = $cursor->getPosition();
|
||||
|
||||
$charBefore = $cursor->peek(-1);
|
||||
if ($charBefore === null) {
|
||||
@@ -58,28 +64,15 @@ final class QuoteParser implements InlineParserInterface
|
||||
$canOpen = $leftFlanking && ! $rightFlanking;
|
||||
$canClose = $rightFlanking;
|
||||
|
||||
$node = new Quote($normalizedCharacter, ['delim' => true]);
|
||||
$node = new Quote($char, ['delim' => true]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack to this opener
|
||||
$inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose));
|
||||
$inlineContext->getDelimiterStack()->push(new Delimiter($char, 1, $node, $canOpen, $canClose, $index));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getNormalizedQuoteCharacter(string $character): string
|
||||
{
|
||||
if (\in_array($character, self::DOUBLE_QUOTES, true)) {
|
||||
return Quote::DOUBLE_QUOTE;
|
||||
}
|
||||
|
||||
if (\in_array($character, self::SINGLE_QUOTES, true)) {
|
||||
return Quote::SINGLE_QUOTE;
|
||||
}
|
||||
|
||||
return $character;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool[]
|
||||
*/
|
||||
|
||||
@@ -14,10 +14,10 @@ declare(strict_types=1);
|
||||
namespace League\CommonMark\Extension\Strikethrough;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterface
|
||||
final class StrikethroughDelimiterProcessor implements CacheableDelimiterProcessorInterface
|
||||
{
|
||||
public function getOpeningCharacter(): string
|
||||
{
|
||||
@@ -44,7 +44,8 @@ final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterfa
|
||||
return 0;
|
||||
}
|
||||
|
||||
return \min($opener->getLength(), $closer->getLength());
|
||||
// $opener and $closer are the same length so we just return one of them
|
||||
return $opener->getLength();
|
||||
}
|
||||
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
|
||||
@@ -60,4 +61,9 @@ final class StrikethroughDelimiterProcessor implements DelimiterProcessorInterfa
|
||||
|
||||
$opener->insertAfter($strikethrough);
|
||||
}
|
||||
|
||||
public function getCacheKey(DelimiterInterface $closer): string
|
||||
{
|
||||
return '~' . $closer->getLength();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ final class TableExtension implements ConfigurableExtensionInterface
|
||||
'center' => (clone $attributeArraySchema)->default(['align' => 'center']),
|
||||
'right' => (clone $attributeArraySchema)->default(['align' => 'right']),
|
||||
]),
|
||||
'max_autocompleted_cells' => Expect::int()->min(0)->default(TableParser::DEFAULT_MAX_AUTOCOMPLETED_CELLS),
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -52,7 +53,7 @@ final class TableExtension implements ConfigurableExtensionInterface
|
||||
}
|
||||
|
||||
$environment
|
||||
->addBlockStartParser(new TableStartParser())
|
||||
->addBlockStartParser(new TableStartParser($environment->getConfiguration()->get('table/max_autocompleted_cells')))
|
||||
|
||||
->addRenderer(Table::class, $tableRenderer)
|
||||
->addRenderer(TableSection::class, new TableSectionRenderer())
|
||||
|
||||
@@ -25,6 +25,11 @@ use League\CommonMark\Util\ArrayCollection;
|
||||
|
||||
final class TableParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public const DEFAULT_MAX_AUTOCOMPLETED_CELLS = 10_000;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private Table $block;
|
||||
|
||||
@@ -54,6 +59,8 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private bool $nextIsSeparatorLine = true;
|
||||
|
||||
private int $remainingAutocompletedCells;
|
||||
|
||||
/**
|
||||
* @param array<int, string|null> $columns
|
||||
* @param array<int, string> $headerCells
|
||||
@@ -62,12 +69,13 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
|
||||
*
|
||||
* @phpstan-param array<int, TableCell::ALIGN_*|null> $columns
|
||||
*/
|
||||
public function __construct(array $columns, array $headerCells)
|
||||
public function __construct(array $columns, array $headerCells, int $remainingAutocompletedCells = self::DEFAULT_MAX_AUTOCOMPLETED_CELLS)
|
||||
{
|
||||
$this->block = new Table();
|
||||
$this->bodyLines = new ArrayCollection();
|
||||
$this->columns = $columns;
|
||||
$this->headerCells = $headerCells;
|
||||
$this->block = new Table();
|
||||
$this->bodyLines = new ArrayCollection();
|
||||
$this->columns = $columns;
|
||||
$this->headerCells = $headerCells;
|
||||
$this->remainingAutocompletedCells = $remainingAutocompletedCells;
|
||||
}
|
||||
|
||||
public function canHaveLazyContinuationLines(): bool
|
||||
@@ -121,6 +129,12 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
|
||||
|
||||
// Body can not have more columns than head
|
||||
for ($i = 0; $i < $headerColumns; $i++) {
|
||||
// It can have less columns though, in which case we'll autocomplete the empty ones (up to some limit)
|
||||
if (! isset($cells[$i]) && $this->remainingAutocompletedCells-- <= 0) {
|
||||
// Too many cells were auto-completed, so we'll just stop here
|
||||
return;
|
||||
}
|
||||
|
||||
$cell = $cells[$i] ?? '';
|
||||
$tableCell = $this->parseCell($cell, $i, $inlineParser);
|
||||
$row->appendChild($tableCell);
|
||||
@@ -138,14 +152,12 @@ final class TableParser extends AbstractBlockContinueParser implements BlockCont
|
||||
|
||||
private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
|
||||
{
|
||||
$tableCell = new TableCell();
|
||||
$tableCell = new TableCell(TableCell::TYPE_DATA, $this->columns[$column] ?? null);
|
||||
|
||||
if ($column < \count($this->columns)) {
|
||||
$tableCell->setAlign($this->columns[$column]);
|
||||
if ($cell !== '') {
|
||||
$inlineParser->parse(\trim($cell), $tableCell);
|
||||
}
|
||||
|
||||
$inlineParser->parse(\trim($cell), $tableCell);
|
||||
|
||||
return $tableCell;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,13 @@ use League\CommonMark\Parser\MarkdownParserStateInterface;
|
||||
|
||||
final class TableStartParser implements BlockStartParserInterface
|
||||
{
|
||||
private int $maxAutocompletedCells;
|
||||
|
||||
public function __construct(int $maxAutocompletedCells = TableParser::DEFAULT_MAX_AUTOCOMPLETED_CELLS)
|
||||
{
|
||||
$this->maxAutocompletedCells = $maxAutocompletedCells;
|
||||
}
|
||||
|
||||
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
|
||||
{
|
||||
$paragraph = $parserState->getParagraphContent();
|
||||
@@ -35,8 +42,8 @@ final class TableStartParser implements BlockStartParserInterface
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$lines = \explode("\n", $paragraph);
|
||||
$lastLine = \array_pop($lines);
|
||||
$lastLineBreak = \strrpos($paragraph, "\n");
|
||||
$lastLine = $lastLineBreak === false ? $paragraph : \substr($paragraph, $lastLineBreak + 1);
|
||||
|
||||
$headerCells = TableParser::split($lastLine);
|
||||
if (\count($headerCells) > \count($columns)) {
|
||||
@@ -47,13 +54,13 @@ final class TableStartParser implements BlockStartParserInterface
|
||||
|
||||
$parsers = [];
|
||||
|
||||
if (\count($lines) > 0) {
|
||||
if ($lastLineBreak !== false) {
|
||||
$p = new ParagraphParser();
|
||||
$p->addLine(\implode("\n", $lines));
|
||||
$p->addLine(\substr($paragraph, 0, $lastLineBreak));
|
||||
$parsers[] = $p;
|
||||
}
|
||||
|
||||
$parsers[] = new TableParser($columns, $headerCells);
|
||||
$parsers[] = new TableParser($columns, $headerCells, $this->maxAutocompletedCells);
|
||||
|
||||
return BlockStart::of(...$parsers)
|
||||
->at($cursor)
|
||||
|
||||
@@ -18,4 +18,6 @@ namespace League\CommonMark\Node\Block;
|
||||
|
||||
class Paragraph extends AbstractBlock
|
||||
{
|
||||
/** @internal */
|
||||
public bool $onlyContainsLinkReferenceDefinitions = false;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ final class SlugNormalizer implements TextNormalizerInterface, ConfigurationAwar
|
||||
$slug = \mb_substr($slug, 0, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line Because it thinks mb_substr() returns false on PHP 7.4
|
||||
return $slug;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ final class TextNormalizer implements TextNormalizerInterface
|
||||
$text = \preg_replace('/[ \t\r\n]+/', ' ', \trim($text));
|
||||
\assert(\is_string($text));
|
||||
|
||||
// Is it strictly ASCII? If so, we can use strtolower() instead (faster)
|
||||
if (\mb_check_encoding($text, 'ASCII')) {
|
||||
return \strtolower($text);
|
||||
}
|
||||
|
||||
return \mb_convert_case($text, \MB_CASE_FOLD, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace League\CommonMark\Parser\Block;
|
||||
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Reference\ReferenceMapInterface;
|
||||
|
||||
@@ -50,4 +51,30 @@ final class DocumentBlockParser extends AbstractBlockContinueParser
|
||||
{
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
$this->removeLinkReferenceDefinitions();
|
||||
}
|
||||
|
||||
private function removeLinkReferenceDefinitions(): void
|
||||
{
|
||||
$emptyNodes = [];
|
||||
|
||||
$walker = $this->document->walker();
|
||||
while ($event = $walker->next()) {
|
||||
$node = $event->getNode();
|
||||
// TODO for v3: It would be great if we could find an alternate way to identify such paragraphs.
|
||||
// Unfortunately, we can't simply check for empty paragraphs here because inlines haven't been processed yet,
|
||||
// meaning all paragraphs will appear blank here, and we don't have a way to check the status of the reference parser
|
||||
// which is attached to the (already-closed) paragraph parser.
|
||||
if ($event->isEntering() && $node instanceof Paragraph && $node->onlyContainsLinkReferenceDefinitions) {
|
||||
$emptyNodes[] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($emptyNodes as $node) {
|
||||
$node->detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,7 @@ final class ParagraphParser extends AbstractBlockContinueParser implements Block
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
if ($this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '') {
|
||||
$this->block->detach();
|
||||
}
|
||||
$this->block->onlyContainsLinkReferenceDefinitions = $this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '';
|
||||
}
|
||||
|
||||
public function parseInlines(InlineParserEngineInterface $inlineParser): void
|
||||
|
||||
@@ -322,20 +322,21 @@ class Cursor
|
||||
*/
|
||||
public function advanceToNextNonSpaceOrNewline(): int
|
||||
{
|
||||
$remainder = $this->getRemainder();
|
||||
$currentCharacter = $this->getCurrentCharacter();
|
||||
|
||||
// Optimization: Avoid the regex if we know there are no spaces or newlines
|
||||
if ($remainder === '' || ($remainder[0] !== ' ' && $remainder[0] !== "\n")) {
|
||||
if ($currentCharacter !== ' ' && $currentCharacter !== "\n") {
|
||||
$this->previousPosition = $this->currentPosition;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
\preg_match('/^ *(?:\n *)?/', $remainder, $matches, \PREG_OFFSET_CAPTURE);
|
||||
\preg_match('/^ *(?:\n *)?/', $this->getRemainder(), $matches, \PREG_OFFSET_CAPTURE);
|
||||
|
||||
// [0][0] contains the matched text
|
||||
// [0][1] contains the index of that match
|
||||
\assert(isset($matches[0]));
|
||||
$increment = $matches[0][1] + \strlen($matches[0][0]);
|
||||
|
||||
$this->advanceBy($increment);
|
||||
|
||||
@@ -42,12 +42,12 @@ final class InlineParserContext
|
||||
*/
|
||||
private array $matches;
|
||||
|
||||
public function __construct(Cursor $contents, AbstractBlock $container, ReferenceMapInterface $referenceMap)
|
||||
public function __construct(Cursor $contents, AbstractBlock $container, ReferenceMapInterface $referenceMap, int $maxDelimitersPerLine = PHP_INT_MAX)
|
||||
{
|
||||
$this->referenceMap = $referenceMap;
|
||||
$this->container = $container;
|
||||
$this->cursor = $contents;
|
||||
$this->delimiterStack = new DelimiterStack();
|
||||
$this->delimiterStack = new DelimiterStack($maxDelimitersPerLine);
|
||||
}
|
||||
|
||||
public function getContainer(): AbstractBlock
|
||||
|
||||
@@ -59,7 +59,7 @@ final class InlineParserEngine implements InlineParserEngineInterface
|
||||
$contents = \trim($contents);
|
||||
$cursor = new Cursor($contents);
|
||||
|
||||
$inlineParserContext = new InlineParserContext($cursor, $block, $this->referenceMap);
|
||||
$inlineParserContext = new InlineParserContext($cursor, $block, $this->referenceMap, $this->environment->getConfiguration()->get('max_delimiters_per_line'));
|
||||
|
||||
// Have all parsers look at the line to determine what they might want to parse and what positions they exist at
|
||||
foreach ($this->matchParsers($contents) as $matchPosition => $parsers) {
|
||||
|
||||
@@ -32,6 +32,7 @@ use League\CommonMark\Parser\Block\BlockStart;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Block\DocumentBlockParser;
|
||||
use League\CommonMark\Parser\Block\ParagraphParser;
|
||||
use League\CommonMark\Reference\MemoryLimitedReferenceMap;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceMap;
|
||||
|
||||
@@ -102,7 +103,7 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
|
||||
// finalizeAndProcess
|
||||
$this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber);
|
||||
$this->processInlines();
|
||||
$this->processInlines(\strlen($input));
|
||||
|
||||
$this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
|
||||
|
||||
@@ -115,6 +116,9 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
*/
|
||||
private function parseLine(string $line): void
|
||||
{
|
||||
// replace NUL characters for security
|
||||
$line = \str_replace("\0", "\u{FFFD}", $line);
|
||||
|
||||
$this->cursor = new Cursor($line);
|
||||
|
||||
$matches = $this->parseBlockContinuation();
|
||||
@@ -158,12 +162,13 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
$unmatchedBlocks = 0;
|
||||
}
|
||||
|
||||
$oldBlockLineStart = null;
|
||||
if ($blockStart->isReplaceActiveBlockParser()) {
|
||||
$this->prepareActiveBlockParserForReplacement();
|
||||
$oldBlockLineStart = $this->prepareActiveBlockParserForReplacement();
|
||||
}
|
||||
|
||||
foreach ($blockStart->getBlockParsers() as $newBlockParser) {
|
||||
$blockParser = $this->addChild($newBlockParser);
|
||||
$blockParser = $this->addChild($newBlockParser, $oldBlockLineStart);
|
||||
$tryBlockStarts = $newBlockParser->isContainer();
|
||||
}
|
||||
}
|
||||
@@ -176,7 +181,7 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
} else {
|
||||
// finalize any blocks not matched
|
||||
if ($unmatchedBlocks > 0) {
|
||||
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber);
|
||||
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
|
||||
}
|
||||
|
||||
if (! $blockParser->isContainer()) {
|
||||
@@ -262,9 +267,9 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
/**
|
||||
* Walk through a block & children recursively, parsing string content into inline content where appropriate.
|
||||
*/
|
||||
private function processInlines(): void
|
||||
private function processInlines(int $inputSize): void
|
||||
{
|
||||
$p = new InlineParserEngine($this->environment, $this->referenceMap);
|
||||
$p = new InlineParserEngine($this->environment, new MemoryLimitedReferenceMap($this->referenceMap, $inputSize));
|
||||
|
||||
foreach ($this->closedBlockParsers as $blockParser) {
|
||||
$blockParser->parseInlines($p);
|
||||
@@ -275,12 +280,12 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
* Add block of type tag as a child of the tip. If the tip can't accept children, close and finalize it and try
|
||||
* its parent, and so on til we find a block that can accept children.
|
||||
*/
|
||||
private function addChild(BlockContinueParserInterface $blockParser): BlockContinueParserInterface
|
||||
private function addChild(BlockContinueParserInterface $blockParser, ?int $startLineNumber = null): BlockContinueParserInterface
|
||||
{
|
||||
$blockParser->getBlock()->setStartLine($this->lineNumber);
|
||||
$blockParser->getBlock()->setStartLine($startLineNumber ?? $this->lineNumber);
|
||||
|
||||
while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
|
||||
$this->closeBlockParsers(1, $this->lineNumber - 1);
|
||||
$this->closeBlockParsers(1, ($startLineNumber ?? $this->lineNumber) - 1);
|
||||
}
|
||||
|
||||
$this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
|
||||
@@ -307,7 +312,10 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
return $popped;
|
||||
}
|
||||
|
||||
private function prepareActiveBlockParserForReplacement(): void
|
||||
/**
|
||||
* @return int|null The line number where the old block started
|
||||
*/
|
||||
private function prepareActiveBlockParserForReplacement(): ?int
|
||||
{
|
||||
// Note that we don't want to parse inlines or finalize this block, as it's getting replaced.
|
||||
$old = $this->deactivateBlockParser();
|
||||
@@ -317,6 +325,8 @@ final class MarkdownParser implements MarkdownParserInterface
|
||||
}
|
||||
|
||||
$old->getBlock()->detach();
|
||||
|
||||
return $old->getBlock()->getStartLine();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
68
vendor/league/commonmark/src/Reference/MemoryLimitedReferenceMap.php
vendored
Normal file
68
vendor/league/commonmark/src/Reference/MemoryLimitedReferenceMap.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Reference;
|
||||
|
||||
final class MemoryLimitedReferenceMap implements ReferenceMapInterface
|
||||
{
|
||||
private ReferenceMapInterface $decorated;
|
||||
|
||||
private const MINIMUM_SIZE = 100_000;
|
||||
|
||||
private int $remaining;
|
||||
|
||||
public function __construct(ReferenceMapInterface $decorated, int $maxSize)
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
$this->remaining = \max(self::MINIMUM_SIZE, $maxSize);
|
||||
}
|
||||
|
||||
public function add(ReferenceInterface $reference): void
|
||||
{
|
||||
$this->decorated->add($reference);
|
||||
}
|
||||
|
||||
public function contains(string $label): bool
|
||||
{
|
||||
return $this->decorated->contains($label);
|
||||
}
|
||||
|
||||
public function get(string $label): ?ReferenceInterface
|
||||
{
|
||||
$reference = $this->decorated->get($label);
|
||||
if ($reference === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for expansion limit
|
||||
$this->remaining -= \strlen($reference->getDestination()) + \strlen($reference->getTitle());
|
||||
if ($this->remaining < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Traversable<string, ReferenceInterface>
|
||||
*/
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
return $this->decorated->getIterator();
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return $this->decorated->count();
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,10 @@ final class ReferenceMap implements ReferenceMapInterface
|
||||
|
||||
public function contains(string $label): bool
|
||||
{
|
||||
if ($this->references === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$label = $this->normalizer->normalize($label);
|
||||
|
||||
return isset($this->references[$label]);
|
||||
@@ -55,6 +59,10 @@ final class ReferenceMap implements ReferenceMapInterface
|
||||
|
||||
public function get(string $label): ?ReferenceInterface
|
||||
{
|
||||
if ($this->references === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$label = $this->normalizer->normalize($label);
|
||||
|
||||
return $this->references[$label] ?? null;
|
||||
|
||||
@@ -30,15 +30,8 @@ final class LinkParserHelper
|
||||
*/
|
||||
public static function parseLinkDestination(Cursor $cursor): ?string
|
||||
{
|
||||
if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) {
|
||||
// Chop off surrounding <..>:
|
||||
return UrlEncoder::unescapeAndEncode(
|
||||
RegexHelper::unescape(\substr($res, 1, -1))
|
||||
);
|
||||
}
|
||||
|
||||
if ($cursor->getCurrentCharacter() === '<') {
|
||||
return null;
|
||||
return self::parseDestinationBraces($cursor);
|
||||
}
|
||||
|
||||
$destination = self::manuallyParseLinkDestination($cursor);
|
||||
@@ -69,7 +62,7 @@ final class LinkParserHelper
|
||||
|
||||
public static function parsePartialLinkLabel(Cursor $cursor): ?string
|
||||
{
|
||||
return $cursor->match('/^(?:[^\\\\\[\]]+|\\\\.?)*/');
|
||||
return $cursor->match('/^(?:[^\\\\\[\]]++|\\\\.?)*+/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,27 +93,27 @@ final class LinkParserHelper
|
||||
|
||||
private static function manuallyParseLinkDestination(Cursor $cursor): ?string
|
||||
{
|
||||
$oldPosition = $cursor->getPosition();
|
||||
$oldState = $cursor->saveState();
|
||||
|
||||
$remainder = $cursor->getRemainder();
|
||||
$openParens = 0;
|
||||
while (($c = $cursor->getCurrentCharacter()) !== null) {
|
||||
if ($c === '\\' && ($peek = $cursor->peek()) !== null && RegexHelper::isEscapable($peek)) {
|
||||
$cursor->advanceBy(2);
|
||||
$len = \strlen($remainder);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$c = $remainder[$i];
|
||||
if ($c === '\\' && $i + 1 < $len && RegexHelper::isEscapable($remainder[$i + 1])) {
|
||||
$i++;
|
||||
} elseif ($c === '(') {
|
||||
$cursor->advanceBy(1);
|
||||
$openParens++;
|
||||
// Limit to 32 nested parens for pathological cases
|
||||
if ($openParens > 32) {
|
||||
return null;
|
||||
}
|
||||
} elseif ($c === ')') {
|
||||
if ($openParens < 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
$openParens--;
|
||||
} elseif (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $c)) {
|
||||
} elseif (\ord($c) <= 32 && RegexHelper::isWhitespace($c)) {
|
||||
break;
|
||||
} else {
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,15 +121,45 @@ final class LinkParserHelper
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($cursor->getPosition() === $oldPosition && (! isset($c) || $c !== ')')) {
|
||||
if ($i === 0 && (! isset($c) || $c !== ')')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$newPos = $cursor->getPosition();
|
||||
$cursor->restoreState($oldState);
|
||||
$destination = \substr($remainder, 0, $i);
|
||||
$cursor->advanceBy(\mb_strlen($destination, 'UTF-8'));
|
||||
|
||||
$cursor->advanceBy($newPos - $cursor->getPosition());
|
||||
return $destination;
|
||||
}
|
||||
|
||||
return $cursor->getPreviousText();
|
||||
/** @var \WeakReference<Cursor>|null */
|
||||
private static ?\WeakReference $lastCursor = null;
|
||||
private static bool $lastCursorLacksClosingBrace = false;
|
||||
|
||||
private static function parseDestinationBraces(Cursor $cursor): ?string
|
||||
{
|
||||
// Optimization: If we've previously parsed this cursor and returned `null`, we know
|
||||
// that no closing brace exists, so we can skip the regex entirely. This helps avoid
|
||||
// certain pathological cases where the regex engine can take a very long time to
|
||||
// determine that no match exists.
|
||||
if (self::$lastCursor !== null && self::$lastCursor->get() === $cursor) {
|
||||
if (self::$lastCursorLacksClosingBrace) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
self::$lastCursor = \WeakReference::create($cursor);
|
||||
}
|
||||
|
||||
if ($res = $cursor->match(RegexHelper::REGEX_LINK_DESTINATION_BRACES)) {
|
||||
self::$lastCursorLacksClosingBrace = false;
|
||||
|
||||
// Chop off surrounding <..>:
|
||||
return UrlEncoder::unescapeAndEncode(
|
||||
RegexHelper::unescape(\substr($res, 1, -1))
|
||||
);
|
||||
}
|
||||
|
||||
self::$lastCursorLacksClosingBrace = true;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ final class RegexHelper
|
||||
public const PARTIAL_REG_CHAR = '[^\\\\()\x00-\x20]';
|
||||
public const PARTIAL_IN_PARENS_NOSP = '\((' . self::PARTIAL_REG_CHAR . '|' . self::PARTIAL_ESCAPED_CHAR . '|\\\\)*\)';
|
||||
public const PARTIAL_TAGNAME = '[a-z][a-z0-9-]*';
|
||||
public const PARTIAL_BLOCKTAGNAME = '(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)';
|
||||
public const PARTIAL_BLOCKTAGNAME = '(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)';
|
||||
public const PARTIAL_ATTRIBUTENAME = '[a-z_:][a-z0-9:._-]*';
|
||||
public const PARTIAL_UNQUOTEDVALUE = '[^"\'=<>`\x00-\x20]+';
|
||||
public const PARTIAL_SINGLEQUOTEDVALUE = '\'[^\']*\'';
|
||||
@@ -53,19 +53,19 @@ final class RegexHelper
|
||||
public const PARTIAL_CLOSETAG = '<\/' . self::PARTIAL_TAGNAME . '\s*[>]';
|
||||
public const PARTIAL_OPENBLOCKTAG = '<' . self::PARTIAL_BLOCKTAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>';
|
||||
public const PARTIAL_CLOSEBLOCKTAG = '<\/' . self::PARTIAL_BLOCKTAGNAME . '\s*[>]';
|
||||
public const PARTIAL_HTMLCOMMENT = '<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->';
|
||||
public const PARTIAL_HTMLCOMMENT = '<!-->|<!--->|<!--[\s\S]*?-->';
|
||||
public const PARTIAL_PROCESSINGINSTRUCTION = '[<][?][\s\S]*?[?][>]';
|
||||
public const PARTIAL_DECLARATION = '<![A-Z]+' . '\s+[^>]*>';
|
||||
public const PARTIAL_DECLARATION = '<![A-Za-z]+' . '[^>]*>';
|
||||
public const PARTIAL_CDATA = '<!\[CDATA\[[\s\S]*?]\]>';
|
||||
public const PARTIAL_HTMLTAG = '(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . '|' . self::PARTIAL_HTMLCOMMENT . '|' .
|
||||
self::PARTIAL_PROCESSINGINSTRUCTION . '|' . self::PARTIAL_DECLARATION . '|' . self::PARTIAL_CDATA . ')';
|
||||
public const PARTIAL_HTMLBLOCKOPEN = '<(?:' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s\/>]|$)' . '|' .
|
||||
'\/' . self::PARTIAL_BLOCKTAGNAME . '(?:[\s>]|$)' . '|' . '[?!])';
|
||||
public const PARTIAL_LINK_TITLE = '^(?:"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"' .
|
||||
'|' . '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\'' .
|
||||
'|' . '\((' . self::PARTIAL_ESCAPED_CHAR . '|[^()\x00])*\))';
|
||||
public const PARTIAL_LINK_TITLE = '^(?:"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*+"' .
|
||||
'|' . '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*+\'' .
|
||||
'|' . '\((' . self::PARTIAL_ESCAPED_CHAR . '|[^()\x00])*+\))';
|
||||
|
||||
public const REGEX_PUNCTUATION = '/^[\x{2000}-\x{206F}\x{2E00}-\x{2E7F}\p{Pc}\p{Pd}\p{Pe}\p{Pf}\p{Pi}\p{Po}\p{Ps}\\\\\'!"#\$%&\(\)\*\+,\-\.\\/:;<=>\?@\[\]\^_`\{\|\}~]/u';
|
||||
public const REGEX_PUNCTUATION = '/^[!"#$%&\'()*+,\-.\\/:;<=>?@\\[\\]\\\\^_`{|}~\p{P}\p{S}]/u';
|
||||
public const REGEX_UNSAFE_PROTOCOL = '/^javascript:|vbscript:|file:|data:/i';
|
||||
public const REGEX_SAFE_DATA_PROTOCOL = '/^data:image\/(?:png|gif|jpeg|webp)/i';
|
||||
public const REGEX_NON_SPACE = '/[^ \t\f\v\r\n]/';
|
||||
@@ -83,6 +83,12 @@ final class RegexHelper
|
||||
return \preg_match('/' . self::PARTIAL_ESCAPABLE . '/', $character) === 1;
|
||||
}
|
||||
|
||||
public static function isWhitespace(string $character): bool
|
||||
{
|
||||
/** @psalm-suppress InvalidLiteralArgument */
|
||||
return $character !== '' && \strpos(" \t\n\x0b\x0c\x0d", $character) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
|
||||
@@ -40,6 +40,7 @@ final class SpecReader
|
||||
$exampleNumber = 0;
|
||||
|
||||
foreach ($matches as $match) {
|
||||
\assert(isset($match[1], $match[2], $match[3]));
|
||||
if (isset($match[4])) {
|
||||
$currentSection = $match[4];
|
||||
continue;
|
||||
|
||||
2
vendor/league/flysystem-local/LICENSE
vendored
2
vendor/league/flysystem-local/LICENSE
vendored
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2023 Frank de Jonge
|
||||
Copyright (c) 2013-2024 Frank de Jonge
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -71,10 +71,10 @@ class LocalFilesystemAdapter implements FilesystemAdapter, ChecksumProvider
|
||||
|
||||
public function __construct(
|
||||
string $location,
|
||||
VisibilityConverter $visibility = null,
|
||||
?VisibilityConverter $visibility = null,
|
||||
private int $writeFlags = LOCK_EX,
|
||||
private int $linkHandling = self::DISALLOW_LINKS,
|
||||
MimeTypeDetector $mimeTypeDetector = null,
|
||||
?MimeTypeDetector $mimeTypeDetector = null,
|
||||
bool $lazyRootCreation = false,
|
||||
bool $useInconclusiveMimeTypeFallback = false,
|
||||
) {
|
||||
@@ -271,7 +271,7 @@ class LocalFilesystemAdapter implements FilesystemAdapter, ChecksumProvider
|
||||
$this->resolveDirectoryVisibility($config->get(Config::OPTION_DIRECTORY_VISIBILITY))
|
||||
);
|
||||
|
||||
if ( ! @copy($sourcePath, $destinationPath)) {
|
||||
if ($sourcePath !== $destinationPath && ! @copy($sourcePath, $destinationPath)) {
|
||||
throw UnableToCopyFile::because(error_get_last()['message'] ?? 'unknown', $source, $destination);
|
||||
}
|
||||
|
||||
|
||||
2
vendor/league/flysystem/LICENSE
vendored
2
vendor/league/flysystem/LICENSE
vendored
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013-2023 Frank de Jonge
|
||||
Copyright (c) 2013-2024 Frank de Jonge
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
9
vendor/league/flysystem/composer.json
vendored
9
vendor/league/flysystem/composer.json
vendored
@@ -25,17 +25,20 @@
|
||||
"ext-zip": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-ftp": "*",
|
||||
"ext-mongodb": "^1.3",
|
||||
"microsoft/azure-storage-blob": "^1.1",
|
||||
"phpunit/phpunit": "^9.5.11|^10.0",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpseclib/phpseclib": "^3.0.34",
|
||||
"aws/aws-sdk-php": "^3.220.0",
|
||||
"phpseclib/phpseclib": "^3.0.36",
|
||||
"aws/aws-sdk-php": "^3.295.10",
|
||||
"composer/semver": "^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.5",
|
||||
"google/cloud-storage": "^1.23",
|
||||
"async-aws/s3": "^1.5 || ^2.0",
|
||||
"async-aws/simple-s3": "^1.1 || ^2.0",
|
||||
"sabre/dav": "^4.3.1"
|
||||
"mongodb/mongodb": "^1.2",
|
||||
"sabre/dav": "^4.6.0",
|
||||
"guzzlehttp/psr7": "^2.6"
|
||||
},
|
||||
"conflict": {
|
||||
"async-aws/core": "<1.19.0",
|
||||
|
||||
2
vendor/league/flysystem/readme.md
vendored
2
vendor/league/flysystem/readme.md
vendored
@@ -32,6 +32,7 @@ for which ever storage is right for you.
|
||||
* **[AsyncAws S3](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/)**
|
||||
* **[Google Cloud Storage](https://flysystem.thephpleague.com/docs/adapter/google-cloud-storage/)**
|
||||
* **[Azure Blob Storage](https://flysystem.thephpleague.com/docs/adapter/azure-blob-storage/)**
|
||||
* **[MongoDB GridFS](https://flysystem.thephpleague.com/docs/adapter/gridfs/)**
|
||||
* **[WebDAV](https://flysystem.thephpleague.com/docs/adapter/webdav/)**
|
||||
* **[ZipArchive](https://flysystem.thephpleague.com/docs/adapter/zip-archive/)**
|
||||
|
||||
@@ -45,6 +46,7 @@ for which ever storage is right for you.
|
||||
* **[Dropbox](https://github.com/spatie/flysystem-dropbox)**
|
||||
* **[ReplicateAdapter](https://github.com/ajgarlag/flysystem-replicate)**
|
||||
* **[Uploadcare](https://github.com/vormkracht10/flysystem-uploadcare)**
|
||||
* **[Useful adapters (FallbackAdapter, LogAdapter, ReadWriteAdapter, RetryAdapter)](https://github.com/ElGigi/FlysystemUsefulAdapters)**
|
||||
|
||||
You can always [create an adapter](https://flysystem.thephpleague.com/docs/advanced/creating-an-adapter/) yourself.
|
||||
|
||||
|
||||
17
vendor/league/flysystem/src/Filesystem.php
vendored
17
vendor/league/flysystem/src/Filesystem.php
vendored
@@ -25,7 +25,7 @@ class Filesystem implements FilesystemOperator
|
||||
public function __construct(
|
||||
private FilesystemAdapter $adapter,
|
||||
array $config = [],
|
||||
PathNormalizer $pathNormalizer = null,
|
||||
?PathNormalizer $pathNormalizer = null,
|
||||
private ?PublicUrlGenerator $publicUrlGenerator = null,
|
||||
private ?TemporaryUrlGenerator $temporaryUrlGenerator = null,
|
||||
) {
|
||||
@@ -187,7 +187,10 @@ class Filesystem implements FilesystemOperator
|
||||
?? throw UnableToGeneratePublicUrl::noGeneratorConfigured($path);
|
||||
$config = $this->config->extend($config);
|
||||
|
||||
return $this->publicUrlGenerator->publicUrl($this->pathNormalizer->normalizePath($path), $config);
|
||||
return $this->publicUrlGenerator->publicUrl(
|
||||
$this->pathNormalizer->normalizePath($path),
|
||||
$config,
|
||||
);
|
||||
}
|
||||
|
||||
public function temporaryUrl(string $path, DateTimeInterface $expiresAt, array $config = []): string
|
||||
@@ -214,9 +217,15 @@ class Filesystem implements FilesystemOperator
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->adapter->checksum($path, $config);
|
||||
return $this->adapter->checksum(
|
||||
$this->pathNormalizer->normalizePath($path),
|
||||
$config,
|
||||
);
|
||||
} catch (ChecksumAlgoIsNotSupported) {
|
||||
return $this->calculateChecksumFromStream($path, $config);
|
||||
return $this->calculateChecksumFromStream(
|
||||
$this->pathNormalizer->normalizePath($path),
|
||||
$config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
vendor/league/flysystem/src/MountManager.php
vendored
27
vendor/league/flysystem/src/MountManager.php
vendored
@@ -7,6 +7,7 @@ namespace League\Flysystem;
|
||||
use DateTimeInterface;
|
||||
use Throwable;
|
||||
|
||||
use function compact;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
|
||||
@@ -33,6 +34,15 @@ class MountManager implements FilesystemOperator
|
||||
$this->config = new Config($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* It is not recommended to mount filesystems after creation because interacting
|
||||
* with the Mount Manager becomes unpredictable. Use this as an escape hatch.
|
||||
*/
|
||||
public function dangerouslyMountFilesystems(string $key, FilesystemOperator $filesystem): void
|
||||
{
|
||||
$this->mountFilesystem($key, $filesystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,FilesystemOperator> $filesystems
|
||||
*/
|
||||
@@ -156,15 +166,15 @@ class MountManager implements FilesystemOperator
|
||||
}
|
||||
}
|
||||
|
||||
public function visibility(string $location): string
|
||||
public function visibility(string $path): string
|
||||
{
|
||||
/** @var FilesystemOperator $filesystem */
|
||||
[$filesystem, $path] = $this->determineFilesystemAndPath($location);
|
||||
[$filesystem, $location] = $this->determineFilesystemAndPath($path);
|
||||
|
||||
try {
|
||||
return $filesystem->visibility($path);
|
||||
return $filesystem->visibility($location);
|
||||
} catch (UnableToRetrieveMetadata $exception) {
|
||||
throw UnableToRetrieveMetadata::visibility($location, $exception->reason(), $exception);
|
||||
throw UnableToRetrieveMetadata::visibility($path, $exception->reason(), $exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,11 +328,7 @@ class MountManager implements FilesystemOperator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $key
|
||||
* @param mixed $filesystem
|
||||
*/
|
||||
private function guardAgainstInvalidMount($key, $filesystem): void
|
||||
private function guardAgainstInvalidMount(mixed $key, mixed $filesystem): void
|
||||
{
|
||||
if ( ! is_string($key)) {
|
||||
throw UnableToMountFilesystem::becauseTheKeyIsNotValid($key);
|
||||
@@ -391,10 +397,11 @@ class MountManager implements FilesystemOperator
|
||||
try {
|
||||
if ($visibility == null && $retainVisibility) {
|
||||
$visibility = $sourceFilesystem->visibility($sourcePath);
|
||||
$config = $config->extend(compact('visibility'));
|
||||
}
|
||||
|
||||
$stream = $sourceFilesystem->readStream($sourcePath);
|
||||
$destinationFilesystem->writeStream($destinationPath, $stream, $visibility ? compact(Config::OPTION_VISIBILITY) : []);
|
||||
$destinationFilesystem->writeStream($destinationPath, $stream, $config->toArray());
|
||||
} catch (UnableToRetrieveMetadata | UnableToReadFile | UnableToWriteFile $exception) {
|
||||
throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class UnableToCheckExistence extends RuntimeException implements FilesystemOpera
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function forLocation(string $path, Throwable $exception = null): static
|
||||
public static function forLocation(string $path, ?Throwable $exception = null): static
|
||||
{
|
||||
return new static("Unable to check existence for: {$path}", 0, $exception);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ final class UnableToCopyFile extends RuntimeException implements FilesystemOpera
|
||||
public static function fromLocationTo(
|
||||
string $sourcePath,
|
||||
string $destinationPath,
|
||||
Throwable $previous = null
|
||||
?Throwable $previous = null
|
||||
): UnableToCopyFile {
|
||||
$e = new static("Unable to copy file from $sourcePath to $destinationPath", 0 , $previous);
|
||||
$e->source = $sourcePath;
|
||||
|
||||
@@ -22,7 +22,7 @@ final class UnableToDeleteDirectory extends RuntimeException implements Filesyst
|
||||
public static function atLocation(
|
||||
string $location,
|
||||
string $reason = '',
|
||||
Throwable $previous = null
|
||||
?Throwable $previous = null
|
||||
): UnableToDeleteDirectory {
|
||||
$e = new static(rtrim("Unable to delete directory located at: {$location}. {$reason}"), 0, $previous);
|
||||
$e->location = $location;
|
||||
|
||||
@@ -19,7 +19,7 @@ final class UnableToDeleteFile extends RuntimeException implements FilesystemOpe
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
public static function atLocation(string $location, string $reason = '', Throwable $previous = null): UnableToDeleteFile
|
||||
public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToDeleteFile
|
||||
{
|
||||
$e = new static(rtrim("Unable to delete file located at: {$location}. {$reason}"), 0, $previous);
|
||||
$e->location = $location;
|
||||
|
||||
@@ -37,7 +37,7 @@ final class UnableToMoveFile extends RuntimeException implements FilesystemOpera
|
||||
public static function fromLocationTo(
|
||||
string $sourcePath,
|
||||
string $destinationPath,
|
||||
Throwable $previous = null
|
||||
?Throwable $previous = null
|
||||
): UnableToMoveFile {
|
||||
$message = $previous?->getMessage() ?? "Unable to move file from $sourcePath to $destinationPath";
|
||||
$e = new static($message, 0, $previous);
|
||||
|
||||
@@ -19,7 +19,7 @@ final class UnableToReadFile extends RuntimeException implements FilesystemOpera
|
||||
*/
|
||||
private $reason = '';
|
||||
|
||||
public static function fromLocation(string $location, string $reason = '', Throwable $previous = null): UnableToReadFile
|
||||
public static function fromLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToReadFile
|
||||
{
|
||||
$e = new static(rtrim("Unable to read file from location: {$location}. {$reason}"), 0, $previous);
|
||||
$e->location = $location;
|
||||
|
||||
@@ -24,27 +24,27 @@ final class UnableToRetrieveMetadata extends RuntimeException implements Filesys
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
public static function lastModified(string $location, string $reason = '', Throwable $previous = null): self
|
||||
public static function lastModified(string $location, string $reason = '', ?Throwable $previous = null): self
|
||||
{
|
||||
return static::create($location, FileAttributes::ATTRIBUTE_LAST_MODIFIED, $reason, $previous);
|
||||
}
|
||||
|
||||
public static function visibility(string $location, string $reason = '', Throwable $previous = null): self
|
||||
public static function visibility(string $location, string $reason = '', ?Throwable $previous = null): self
|
||||
{
|
||||
return static::create($location, FileAttributes::ATTRIBUTE_VISIBILITY, $reason, $previous);
|
||||
}
|
||||
|
||||
public static function fileSize(string $location, string $reason = '', Throwable $previous = null): self
|
||||
public static function fileSize(string $location, string $reason = '', ?Throwable $previous = null): self
|
||||
{
|
||||
return static::create($location, FileAttributes::ATTRIBUTE_FILE_SIZE, $reason, $previous);
|
||||
}
|
||||
|
||||
public static function mimeType(string $location, string $reason = '', Throwable $previous = null): self
|
||||
public static function mimeType(string $location, string $reason = '', ?Throwable $previous = null): self
|
||||
{
|
||||
return static::create($location, FileAttributes::ATTRIBUTE_MIME_TYPE, $reason, $previous);
|
||||
}
|
||||
|
||||
public static function create(string $location, string $type, string $reason = '', Throwable $previous = null): self
|
||||
public static function create(string $location, string $type, string $reason = '', ?Throwable $previous = null): self
|
||||
{
|
||||
$e = new static("Unable to retrieve the $type for file at location: $location. {$reason}", 0, $previous);
|
||||
$e->reason = $reason;
|
||||
|
||||
@@ -27,7 +27,7 @@ final class UnableToSetVisibility extends RuntimeException implements Filesystem
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
public static function atLocation(string $filename, string $extraMessage = '', Throwable $previous = null): self
|
||||
public static function atLocation(string $filename, string $extraMessage = '', ?Throwable $previous = null): self
|
||||
{
|
||||
$message = "Unable to set visibility for file {$filename}. $extraMessage";
|
||||
$e = new static(rtrim($message), 0, $previous);
|
||||
|
||||
@@ -19,7 +19,7 @@ final class UnableToWriteFile extends RuntimeException implements FilesystemOper
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
public static function atLocation(string $location, string $reason = '', Throwable $previous = null): UnableToWriteFile
|
||||
public static function atLocation(string $location, string $reason = '', ?Throwable $previous = null): UnableToWriteFile
|
||||
{
|
||||
$e = new static(rtrim("Unable to write file at location: {$location}. {$reason}"), 0, $previous);
|
||||
$e->location = $location;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 1.16.0 - 2025-09-21
|
||||
|
||||
- Updated lookup
|
||||
- Prepped for 8.4 implicit nullable deprecation
|
||||
|
||||
## 1.15.0 - 2024-01-28
|
||||
|
||||
- Updated lookup
|
||||
|
||||
## 1.14.0 - 2022-10-17
|
||||
|
||||
### Updated
|
||||
|
||||
@@ -13,7 +13,7 @@ class ExtensionMimeTypeDetector implements MimeTypeDetector, ExtensionLookup
|
||||
*/
|
||||
private $extensions;
|
||||
|
||||
public function __construct(ExtensionToMimeTypeMap $extensions = null)
|
||||
public function __construct(?ExtensionToMimeTypeMap $extensions = null)
|
||||
{
|
||||
$this->extensions = $extensions ?: new GeneratedExtensionToMimeTypeMap();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class FinfoMimeTypeDetector implements MimeTypeDetector, ExtensionLookup
|
||||
|
||||
public function __construct(
|
||||
string $magicFile = '',
|
||||
ExtensionToMimeTypeMap $extensionMap = null,
|
||||
?ExtensionToMimeTypeMap $extensionMap = null,
|
||||
?int $bufferSampleSize = null,
|
||||
array $inconclusiveMimetypes = self::INCONCLUSIVE_MIME_TYPES
|
||||
) {
|
||||
|
||||
@@ -82,10 +82,12 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'azv' => 'image/vnd.airzip.accelerator.azv',
|
||||
'azw' => 'application/vnd.amazon.ebook',
|
||||
'b16' => 'image/vnd.pco.b16',
|
||||
'bary' => 'model/vnd.bary',
|
||||
'bat' => 'application/x-msdownload',
|
||||
'bcpio' => 'application/x-bcpio',
|
||||
'bdf' => 'application/x-font-bdf',
|
||||
'bdm' => 'application/vnd.syncml.dm+wbxml',
|
||||
'bdo' => 'application/vnd.nato.bindingdataobject+xml',
|
||||
'bdoc' => 'application/x-bdoc',
|
||||
'bed' => 'application/vnd.realvnc.bed',
|
||||
'bh2' => 'application/vnd.fujitsu.oasysprs',
|
||||
@@ -100,6 +102,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'boz' => 'application/x-bzip2',
|
||||
'bpk' => 'application/octet-stream',
|
||||
'bpmn' => 'application/octet-stream',
|
||||
'brf' => 'application/braille',
|
||||
'bsp' => 'model/vnd.valve.source.compiled-map',
|
||||
'btf' => 'image/prs.btif',
|
||||
'btif' => 'image/prs.btif',
|
||||
@@ -341,6 +344,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'geojson' => 'application/geo+json',
|
||||
'gex' => 'application/vnd.geometry-explorer',
|
||||
'ggb' => 'application/vnd.geogebra.file',
|
||||
'ggs' => 'application/vnd.geogebra.slides',
|
||||
'ggt' => 'application/vnd.geogebra.tool',
|
||||
'ghf' => 'application/vnd.groove-help',
|
||||
'gif' => 'image/gif',
|
||||
@@ -465,6 +469,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'jsonml' => 'application/jsonml+json',
|
||||
'jsx' => 'text/jsx',
|
||||
'jt' => 'model/jt',
|
||||
'jxl' => 'image/jxl',
|
||||
'jxr' => 'image/jxr',
|
||||
'jxra' => 'image/jxra',
|
||||
'jxrs' => 'image/jxrs',
|
||||
@@ -520,6 +525,8 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'lzh' => 'application/octet-stream',
|
||||
'm1v' => 'video/mpeg',
|
||||
'm2a' => 'audio/mpeg',
|
||||
'm2t' => 'video/mp2t',
|
||||
'm2ts' => 'video/mp2t',
|
||||
'm2v' => 'video/mpeg',
|
||||
'm3a' => 'audio/mpeg',
|
||||
'm3u' => 'text/plain',
|
||||
@@ -626,7 +633,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'msp' => 'application/octet-stream',
|
||||
'msty' => 'application/vnd.muvee.style',
|
||||
'mtl' => 'model/mtl',
|
||||
'mts' => 'model/vnd.mts',
|
||||
'mts' => 'video/mp2t',
|
||||
'mus' => 'application/vnd.musician',
|
||||
'musd' => 'application/mmt-usd+xml',
|
||||
'musicxml' => 'application/vnd.recordare.musicxml+xml',
|
||||
@@ -1171,6 +1178,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'xbm' => 'image/x-xbitmap',
|
||||
'xca' => 'application/xcap-caps+xml',
|
||||
'xcs' => 'application/calendar+xml',
|
||||
'xdcf' => 'application/vnd.gov.sk.xmldatacontainer+xml',
|
||||
'xdf' => 'application/xcap-diff+xml',
|
||||
'xdm' => 'application/vnd.syncml.dm+xml',
|
||||
'xdp' => 'application/vnd.adobe.xdp+xml',
|
||||
@@ -1522,6 +1530,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'application/vnd.fuzzysheet' => ['fzs'],
|
||||
'application/vnd.genomatix.tuxedo' => ['txd'],
|
||||
'application/vnd.geogebra.file' => ['ggb'],
|
||||
'application/vnd.geogebra.slides' => ['ggs'],
|
||||
'application/vnd.geogebra.tool' => ['ggt'],
|
||||
'application/vnd.geometry-explorer' => ['gex', 'gre'],
|
||||
'application/vnd.geonext' => ['gxt'],
|
||||
@@ -1533,6 +1542,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'application/vnd.google-apps.spreadsheet' => ['gsheet'],
|
||||
'application/vnd.google-earth.kml+xml' => ['kml'],
|
||||
'application/vnd.google-earth.kmz' => ['kmz'],
|
||||
'application/vnd.gov.sk.xmldatacontainer+xml' => ['xdcf'],
|
||||
'application/vnd.grafeq' => ['gqf', 'gqs'],
|
||||
'application/vnd.groove-account' => ['gac'],
|
||||
'application/vnd.groove-help' => ['ghf'],
|
||||
@@ -1648,6 +1658,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'application/vnd.musician' => ['mus'],
|
||||
'application/vnd.muvee.style' => ['msty'],
|
||||
'application/vnd.mynfc' => ['taglet'],
|
||||
'application/vnd.nato.bindingdataobject+xml' => ['bdo'],
|
||||
'application/vnd.neurolanguage.nlu' => ['nlu'],
|
||||
'application/vnd.nitf' => ['ntf', 'nitf'],
|
||||
'application/vnd.noblenet-directory' => ['nnd'],
|
||||
@@ -2028,6 +2039,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'image/jphc' => ['jhc'],
|
||||
'image/jpm' => ['jpm', 'jpgm'],
|
||||
'image/jpx' => ['jpx', 'jpf'],
|
||||
'image/jxl' => ['jxl'],
|
||||
'image/jxr' => ['jxr'],
|
||||
'image/jxra' => ['jxra'],
|
||||
'image/jxrs' => ['jxrs'],
|
||||
@@ -2110,6 +2122,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'model/step-xml+zip' => ['stpxz'],
|
||||
'model/stl' => ['stl'],
|
||||
'model/u3d' => ['u3d'],
|
||||
'model/vnd.bary' => ['bary'],
|
||||
'model/vnd.cld' => ['cld'],
|
||||
'model/vnd.collada+xml' => ['dae'],
|
||||
'model/vnd.dwf' => ['dwf'],
|
||||
@@ -2207,7 +2220,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'video/jpeg' => ['jpgv'],
|
||||
'video/jpm' => ['jpm', 'jpgm'],
|
||||
'video/mj2' => ['mj2', 'mjp2'],
|
||||
'video/mp2t' => ['ts'],
|
||||
'video/mp2t' => ['ts', 'm2t', 'm2ts', 'mts'],
|
||||
'video/mp4' => ['mp4', 'mp4v', 'mpg4', 'f4v'],
|
||||
'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v'],
|
||||
'video/ogg' => ['ogv'],
|
||||
@@ -2274,6 +2287,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap, Extensi
|
||||
'application/cdr' => ['cdr'],
|
||||
'application/STEP' => ['step', 'stp'],
|
||||
'application/x-ndjson' => ['ndjson'],
|
||||
'application/braille' => ['brf'],
|
||||
];
|
||||
|
||||
public function lookupMimeType(string $extension): ?string
|
||||
|
||||
Reference in New Issue
Block a user