mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-13 11:41:31 +00:00
resolved conflicts
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 PHPSTORM_META
|
||||
{
|
||||
expectedArguments(\League\CommonMark\Util\HtmlElement::__construct(), 0, 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kdb', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr');
|
||||
|
||||
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::__construct(), 0, 1, 2, 3, 4, 5, 6);
|
||||
expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::getLevel(), 1, 2, 3, 4, 5, 6);
|
||||
|
||||
registerArgumentsSet('league_commonmark_htmlblock_types', \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_1_CODE_CONTAINER, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_2_COMMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_3, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_4, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_5_CDATA, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_6_BLOCK_ELEMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_7_MISC_ELEMENT);
|
||||
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::__construct(), 0, argumentsSet('league_commonmark_htmlblock_types'));
|
||||
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::setType(), 0, argumentsSet('league_commonmark_htmlblock_types'));
|
||||
expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::getType(), argumentsSet('league_commonmark_htmlblock_types'));
|
||||
expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockOpenRegex(), 0, argumentsSet('league_commonmark_htmlblock_types'));
|
||||
expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockCloseRegex(), 0, argumentsSet('league_commonmark_htmlblock_types'));
|
||||
|
||||
registerArgumentsSet('league_commonmark_newline_types', \League\CommonMark\Node\Inline\Newline::HARDBREAK, \League\CommonMark\Node\Inline\Newline::SOFTBREAK);
|
||||
expectedArguments(\League\CommonMark\Node\Inline\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types'));
|
||||
expectedReturnValues(\League\CommonMark\Node\Inline\Newline::getType(), argumentsSet('league_commonmark_newline_types'));
|
||||
|
||||
registerArgumentsSet('league_commonmark_options',
|
||||
'html_input',
|
||||
'allow_unsafe_links',
|
||||
'max_nesting_level',
|
||||
'max_delimiters_per_line',
|
||||
'renderer',
|
||||
'renderer/block_separator',
|
||||
'renderer/inner_separator',
|
||||
'renderer/soft_break',
|
||||
'commonmark',
|
||||
'commonmark/enable_em',
|
||||
'commonmark/enable_strong',
|
||||
'commonmark/use_asterisk',
|
||||
'commonmark/use_underscore',
|
||||
'commonmark/unordered_list_markers',
|
||||
'disallowed_raw_html',
|
||||
'disallowed_raw_html/disallowed_tags',
|
||||
'external_link',
|
||||
'external_link/html_class',
|
||||
'external_link/internal_hosts',
|
||||
'external_link/nofollow',
|
||||
'external_link/noopener',
|
||||
'external_link/noreferrer',
|
||||
'external_link/open_in_new_window',
|
||||
'footnote',
|
||||
'footnote/backref_class',
|
||||
'footnote/backref_symbol',
|
||||
'footnote/container_add_hr',
|
||||
'footnote/container_class',
|
||||
'footnote/ref_class',
|
||||
'footnote/ref_id_prefix',
|
||||
'footnote/footnote_class',
|
||||
'footnote/footnote_id_prefix',
|
||||
'heading_permalink',
|
||||
'heading_permalink/apply_id_to_heading',
|
||||
'heading_permalink/heading_class',
|
||||
'heading_permalink/html_class',
|
||||
'heading_permalink/fragment_prefix',
|
||||
'heading_permalink/id_prefix',
|
||||
'heading_permalink/inner_contents',
|
||||
'heading_permalink/insert',
|
||||
'heading_permalink/max_heading_level',
|
||||
'heading_permalink/min_heading_level',
|
||||
'heading_permalink/symbol',
|
||||
'heading_permalink/title',
|
||||
'mentions',
|
||||
'smartpunct/double_quote_closer',
|
||||
'smartpunct/double_quote_opener',
|
||||
'smartpunct/single_quote_closer',
|
||||
'smartpunct/single_quote_opener',
|
||||
'slug_normalizer',
|
||||
'slug_normalizer/instance',
|
||||
'slug_normalizer/max_length',
|
||||
'slug_normalizer/unique',
|
||||
'table',
|
||||
'table/wrap',
|
||||
'table/wrap/attributes',
|
||||
'table/wrap/enabled',
|
||||
'table/wrap/tag',
|
||||
'table/alignment_attributes',
|
||||
'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',
|
||||
'table_of_contents/min_heading_level',
|
||||
'table_of_contents/normalize',
|
||||
'table_of_contents/placeholder',
|
||||
'table_of_contents/position',
|
||||
'table_of_contents/style',
|
||||
);
|
||||
expectedArguments(\League\Config\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options'));
|
||||
expectedArguments(\League\Config\ConfigurationInterface::exists(), 0, argumentsSet('league_commonmark_options'));
|
||||
expectedArguments(\League\Config\MutableConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options'));
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
||||
|
||||
**Upgrading from 1.x?** See <https://commonmark.thephpleague.com/2.0/upgrading/> for additional information.
|
||||
|
||||
## [Unreleased][unreleased]
|
||||
|
||||
## [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
|
||||
|
||||
- Fixed `ExternalLinkProcessor` not fully disabling the `rel` attribute when configured to do so (#992)
|
||||
|
||||
## [2.4.0] - 2023-03-24
|
||||
|
||||
### Added
|
||||
|
||||
- Added generic `CommonMarkException` marker interface for all exceptions thrown by the library
|
||||
- Added several new specific exception types implementing that marker interface:
|
||||
- `AlreadyInitializedException`
|
||||
- `InvalidArgumentException`
|
||||
- `IOException`
|
||||
- `LogicException`
|
||||
- `MissingDependencyException`
|
||||
- `NoMatchingRendererException`
|
||||
- `ParserLogicException`
|
||||
- Added more configuration options to the Heading Permalinks extension (#939):
|
||||
- `heading_permalink/apply_id_to_heading` - When `true`, the `id` attribute will be applied to the heading element itself instead of the `<a>` tag
|
||||
- `heading_permalink/heading_class` - class to apply to the heading element
|
||||
- `heading_permalink/insert` - now accepts `none` to prevent the creation of the `<a>` link
|
||||
- Added new `table/alignment_attributes` configuration option to control how table cell alignment is rendered (#959)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change several thrown exceptions from `RuntimeException` to `LogicException` (or something extending it), including:
|
||||
- `CallbackGenerator`s that fail to set a URL or return an expected value
|
||||
- `MarkdownParser` when deactivating the last block parser or attempting to get an active block parser when they've all been closed
|
||||
- Adding items to an already-initialized `Environment`
|
||||
- Rendering a `Node` when no renderer has been registered for it
|
||||
- `HeadingPermalinkProcessor` now throws `InvalidConfigurationException` instead of `RuntimeException` when invalid config values are given.
|
||||
- `HtmlElement::setAttribute()` no longer requires the second parameter for boolean attributes
|
||||
- Several small micro-optimizations
|
||||
- Changed Strikethrough to only allow 1 or 2 tildes per the updated GFM spec
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed inaccurate `@throws` docblocks throughout the codebase, including `ConverterInterface`, `MarkdownConverter`, and `MarkdownConverterInterface`.
|
||||
- These previously suggested that only `\RuntimeException`s were thrown, which was inaccurate as `\LogicException`s were also possible.
|
||||
|
||||
## [2.3.9] - 2023-02-15
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed autolink extension not detecting some URIs with underscores (#956)
|
||||
|
||||
## [2.3.8] - 2022-12-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed parsing issues when `mb_internal_encoding()` is set to something other than `UTF-8` (#951)
|
||||
|
||||
## [2.3.7] - 2022-11-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `TaskListItemMarkerRenderer` not including HTML attributes set on the node by other extensions (#947)
|
||||
|
||||
## [2.3.6] - 2022-10-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed unquoted attribute parsing when closing curly brace is followed by certain characters (like a `.`) (#943)
|
||||
|
||||
## [2.3.5] - 2022-07-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed error using `InlineParserEngine` when no inline parsers are registered in the `Environment` (#908)
|
||||
|
||||
## [2.3.4] - 2022-07-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Made a number of small tweaks to the embed extension's parsing behavior to fix #898:
|
||||
- Changed `EmbedStartParser` to always capture embed-like lines in container blocks, regardless of parent block type
|
||||
- Changed `EmbedProcessor` to also remove `Embed` blocks that aren't direct children of the `Document`
|
||||
- Increased the priority of `EmbedProcessor` to `1010`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `EmbedExtension` not parsing embeds following a list block (#898)
|
||||
|
||||
## [2.3.3] - 2022-06-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `DomainFilteringAdapter` not reindexing the embed list (#884, #885)
|
||||
|
||||
## [2.3.2] - 2022-06-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881)
|
||||
|
||||
## [2.2.5] - 2022-06-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881)
|
||||
|
||||
## [2.3.1] - 2022-05-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867)
|
||||
|
||||
## [2.2.4] - 2022-05-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867)
|
||||
|
||||
## [2.3.0] - 2022-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added new `EmbedExtension` (#805)
|
||||
- Added `DocumentRendererInterface` as a replacement for the now-deprecated `MarkdownRendererInterface`
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated `MarkdownRendererInterface`; use `DocumentRendererInterface` instead
|
||||
|
||||
## [2.2.3] - 2022-02-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed front matter parsing with Windows line endings (#821)
|
||||
|
||||
## [2.1.3] - 2022-02-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed front matter parsing with Windows line endings (#821)
|
||||
|
||||
## [2.0.4] - 2022-02-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed front matter parsing with Windows line endings (#821)
|
||||
|
||||
## [2.2.2] - 2022-02-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed double-escaping of image alt text (#806, #810)
|
||||
- Fixed Psalm typehints for event class names
|
||||
|
||||
## [2.2.1] - 2022-01-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `symfony/deprecation-contracts` constraint
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed deprecation trigger from `MarkdownConverterInterface` to reduce noise
|
||||
|
||||
## [2.2.0] - 2022-01-22
|
||||
|
||||
### Added
|
||||
|
||||
- Added new `ConverterInterface`
|
||||
- Added new `MarkdownToXmlConverter` class
|
||||
- Added new `HtmlDecorator` class which can wrap existing renderers with additional HTML tags
|
||||
- Added new `table/wrap` config to apply an optional wrapping/container element around a table (#780)
|
||||
|
||||
### Changed
|
||||
|
||||
- `HtmlElement` contents can now consist of any `Stringable`, not just `HtmlElement` and `string`
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecated `MarkdownConverterInterface` and its `convertToHtml()` method; use `ConverterInterface` and `convert()` instead
|
||||
|
||||
## [2.1.2] - 2022-02-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed double-escaping of image alt text (#806, #810)
|
||||
- Fixed Psalm typehints for event class names
|
||||
|
||||
## [2.1.1] - 2022-01-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added missing return type to `Environment::dispatch()` to fix deprecation warning (#778)
|
||||
|
||||
## [2.1.0] - 2021-12-05
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for ext-yaml in FrontMatterExtension (#715)
|
||||
- Added support for symfony/yaml v6.0 in FrontMatterExtension (#739)
|
||||
- Added new `heading_permalink/aria_hidden` config option (#741)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed PHP 8.1 deprecation warning (#759, #762)
|
||||
|
||||
## [2.0.3] - 2022-02-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed double-escaping of image alt text (#806, #810)
|
||||
- Fixed Psalm typehints for event class names
|
||||
|
||||
## [2.0.2] - 2021-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Bumped minimum version of league/config to support PHP 8.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed ability to register block parsers that identify lines starting with letters (#706)
|
||||
|
||||
## [2.0.1] - 2021-07-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed nested autolinks (#689)
|
||||
- Fixed description lists being parsed incorrectly (#692)
|
||||
- Fixed Table of Contents not respecting Heading Permalink prefixes (#690)
|
||||
|
||||
## [2.0.0] - 2021-07-24
|
||||
|
||||
No changes were introduced since the previous RC2 release.
|
||||
See all entries below for a list of changes between 1.x and 2.0.
|
||||
|
||||
## [2.0.0-rc2] - 2021-07-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Mentions inside of links creating nested links against the spec's rules (#688)
|
||||
|
||||
## [2.0.0-rc1] - 2021-07-10
|
||||
|
||||
No changes were introduced since the previous release.
|
||||
|
||||
## [2.0.0-beta3] - 2021-07-03
|
||||
|
||||
### Changed
|
||||
|
||||
- Any leading UTF-8 BOM will be stripped from the input
|
||||
- The `getEnvironment()` method of `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` will always return the concrete, configurable `Environment` for upgrading convenience
|
||||
- Optimized AST iteration
|
||||
- Lots of small micro-optimizations
|
||||
|
||||
## [2.0.0-beta2] - 2021-06-27
|
||||
|
||||
### Added
|
||||
|
||||
- Added new `Node::iterator()` method and `NodeIterator` class for faster AST iteration (#683, #684)
|
||||
|
||||
### Changed
|
||||
|
||||
- Made compatible with CommonMark spec 0.30.0
|
||||
- Optimized link label parsing
|
||||
- Optimized AST iteration for a 50% performance boost in some event listeners (#683, #684)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed processing instructions with EOLs
|
||||
- Fixed case-insensitive matching for HTML tag types
|
||||
- Fixed type 7 HTML blocks incorrectly interrupting lazy paragraphs
|
||||
- Fixed newlines in reference labels not collapsing into spaces
|
||||
- Fixed link label normalization with escaped newlines
|
||||
- Fixed unnecessary AST iteration when no default attributes are configured
|
||||
|
||||
## [2.0.0-beta1] - 2021-06-20
|
||||
|
||||
### Added
|
||||
|
||||
- **Added three new extensions:**
|
||||
- `FrontMatterExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/front-matter/))
|
||||
- `DescriptionListExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/description-lists/))
|
||||
- `DefaultAttributesExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/default-attributes/))
|
||||
- **Added new `XmlRenderer` to simplify AST debugging** ([see documentation](https://commonmark.thephpleague.com/xml/)) (#431)
|
||||
- **Added the ability to configure disallowed raw HTML tags** (#507)
|
||||
- **Added the ability for Mentions to use multiple characters for their symbol** (#514, #550)
|
||||
- **Added the ability to delegate event dispatching to PSR-14 compliant event dispatcher libraries**
|
||||
- **Added new configuration options:**
|
||||
- Added `heading_permalink/min_heading_level` and `heading_permalink/max_heading_level` options to control which headings get permalinks (#519)
|
||||
- Added `heading_permalink/fragment_prefix` to allow customizing the URL fragment prefix (#602)
|
||||
- Added `footnote/backref_symbol` option for customizing backreference link appearance (#522)
|
||||
- Added `slug_normalizer/max_length` option to control the maximum length of generated URL slugs
|
||||
- Added `slug_normalizer/unique` option to control whether unique slugs should be generated per-document or per-environment
|
||||
- **Added purity markers throughout the codebase** (verified with Psalm)
|
||||
- Added `Query` class to simplify Node traversal when looking to take action on certain Nodes
|
||||
- Added new `HtmlFilter` and `StringContainerHelper` utility classes
|
||||
- Added new `AbstractBlockContinueParser` class to simplify the creation of custom block parsers
|
||||
- Added several new classes and interfaces:
|
||||
- `BlockContinue`
|
||||
- `BlockContinueParserInterface`
|
||||
- `BlockContinueParserWithInlinesInterface`
|
||||
- `BlockStart`
|
||||
- `BlockStartParserInterface`
|
||||
- `ChildNodeRendererInterface`
|
||||
- `ConfigurableExtensionInterface`
|
||||
- `CursorState`
|
||||
- `DashParser` (extracted from `PunctuationParser`)
|
||||
- `DelimiterParser`
|
||||
- `DocumentBlockParser`
|
||||
- `DocumentPreRenderEvent`
|
||||
- `DocumentRenderedEvent`
|
||||
- `EllipsesParser` (extracted from `PunctuationParser`)
|
||||
- `ExpressionInterface`
|
||||
- `FallbackNodeXmlRenderer`
|
||||
- `InlineParserEngineInterface`
|
||||
- `InlineParserMatch`
|
||||
- `MarkdownParserState`
|
||||
- `MarkdownParserStateInterface`
|
||||
- `MarkdownRendererInterface`
|
||||
- `Query`
|
||||
- `RawMarkupContainerInterface`
|
||||
- `ReferenceableInterface`
|
||||
- `RenderedContent`
|
||||
- `RenderedContentInterface`
|
||||
- `ReplaceUnpairedQuotesListener`
|
||||
- `SpecReader`
|
||||
- `TableOfContentsRenderer`
|
||||
- `UniqueSlugNormalizer`
|
||||
- `UniqueSlugNormalizerInterface`
|
||||
- `XmlRenderer`
|
||||
- `XmlNodeRendererInterface`
|
||||
- Added several new methods:
|
||||
- `Cursor::getCurrentCharacter()`
|
||||
- `Environment::createDefaultConfiguration()`
|
||||
- `Environment::setEventDispatcher()`
|
||||
- `EnvironmentInterface::getExtensions()`
|
||||
- `EnvironmentInterface::getInlineParsers()`
|
||||
- `EnvironmentInterface::getSlugNormalizer()`
|
||||
- `FencedCode::setInfo()`
|
||||
- `Heading::setLevel()`
|
||||
- `HtmlRenderer::renderDocument()`
|
||||
- `InlineParserContext::getFullMatch()`
|
||||
- `InlineParserContext::getFullMatchLength()`
|
||||
- `InlineParserContext::getMatches()`
|
||||
- `InlineParserContext::getSubMatches()`
|
||||
- `LinkParserHelper::parsePartialLinkLabel()`
|
||||
- `LinkParserHelper::parsePartialLinkTitle()`
|
||||
- `Node::assertInstanceOf()`
|
||||
- `RegexHelper::isLetter()`
|
||||
- `StringContainerInterface::setLiteral()`
|
||||
- `TableCell::getType()`
|
||||
- `TableCell::setType()`
|
||||
- `TableCell::getAlign()`
|
||||
- `TableCell::setAlign()`
|
||||
|
||||
### Changed
|
||||
|
||||
- **Changed the converter return type**
|
||||
- `CommonMarkConverter::convertToHtml()` now returns an instance of `RenderedContentInterface`. This can be cast to a string for backward compatibility with 1.x.
|
||||
- **Table of Contents items are no longer wrapped with `<p>` tags** (#613)
|
||||
- **Heading Permalinks now link to element IDs instead of using `name` attributes** (#602)
|
||||
- **Heading Permalink IDs and URL fragments now have a `content` prefix by default** (#602)
|
||||
- **Changes to configuration options:**
|
||||
- `enable_em` has been renamed to `commonmark/enable_em`
|
||||
- `enable_strong` has been renamed to `commonmark/enable_strong`
|
||||
- `use_asterisk` has been renamed to `commonmark/use_asterisk`
|
||||
- `use_underscore` has been renamed to `commonmark/use_underscore`
|
||||
- `unordered_list_markers` has been renamed to `commonmark/unordered_list_markers`
|
||||
- `mentions/*/symbol` has been renamed to `mentions/*/prefix`
|
||||
- `mentions/*/regex` has been renamed to `mentions/*/pattern` and requires partial regular expressions (without delimiters or flags)
|
||||
- `max_nesting_level` now defaults to `PHP_INT_MAX` and no longer supports floats
|
||||
- `heading_permalink/slug_normalizer` has been renamed to `slug_normalizer/instance`
|
||||
- **Event dispatching is now fully PSR-14 compliant**
|
||||
- **Moved and renamed several classes** - [see the full list here](https://commonmark.thephpleague.com/2.0/upgrading/#classesnamespaces-renamed)
|
||||
- The `HeadingPermalinkExtension` and `FootnoteExtension` were modified to ensure they never produce a slug which conflicts with slugs created by the other extension
|
||||
- `SlugNormalizer::normalizer()` now supports optional prefixes and max length options passed in via the `$context` argument
|
||||
- The `AbstractBlock::$data` and `AbstractInline::$data` arrays were replaced with a `Data` array-like object on the base `Node` class
|
||||
- **Implemented a new approach to block parsing.** This was a massive change, so here are the highlights:
|
||||
- Functionality previously found in block parsers and node elements has moved to block parser factories and block parsers, respectively ([more details](https://commonmark.thephpleague.com/2.0/upgrading/#new-block-parsing-approach))
|
||||
- `ConfigurableEnvironmentInterface::addBlockParser()` is now `EnvironmentBuilderInterface::addBlockParserFactory()`
|
||||
- `ReferenceParser` was re-implemented and works completely different than before
|
||||
- The paragraph parser no longer needs to be added manually to the environment
|
||||
- **Implemented a new approach to inline parsing** where parsers can now specify longer strings or regular expressions they want to parse (instead of just single characters):
|
||||
- `InlineParserInterface::getCharacters()` is now `getMatchDefinition()` and returns an instance of `InlineParserMatch`
|
||||
- `InlineParserContext::__construct()` now requires the contents to be provided as a `Cursor` instead of a `string`
|
||||
- **Implemented delimiter parsing as a special type of inline parser** (via the new `DelimiterParser` class)
|
||||
- **Changed block and inline rendering to use common methods and interfaces**
|
||||
- `BlockRendererInterface` and `InlineRendererInterface` were replaced by `NodeRendererInterface` with slightly different parameters. All core renderers now implement this interface.
|
||||
- `ConfigurableEnvironmentInterface::addBlockRenderer()` and `addInlineRenderer()` were combined into `EnvironmentBuilderInterface::addRenderer()`
|
||||
- `EnvironmentInterface::getBlockRenderersForClass()` and `getInlineRenderersForClass()` are now just `getRenderersForClass()`
|
||||
- **Completely refactored the Configuration implementation**
|
||||
- All configuration-specific classes have been moved into a new `league/config` package with a new namespace
|
||||
- `Configuration` objects must now be configured with a schema and all options must match that schema - arbitrary keys are no longer permitted
|
||||
- `Configuration::__construct()` no longer accepts the default configuration values - use `Configuration::merge()` instead
|
||||
- `ConfigurationInterface` now only contains a `get(string $key)`; this method no longer allows arbitrary default values to be returned if the option is missing
|
||||
- `ConfigurableEnvironmentInterface` was renamed to `EnvironmentBuilderInterface`
|
||||
- `ExtensionInterface::register()` now requires an `EnvironmentBuilderInterface` param instead of `ConfigurableEnvironmentInterface`
|
||||
- **Added missing return types to virtually every class and interface method**
|
||||
- Re-implemented the GFM Autolink extension using the new inline parser approach instead of document processors
|
||||
- `EmailAutolinkProcessor` is now `EmailAutolinkParser`
|
||||
- `UrlAutolinkProcessor` is now `UrlAutolinkParser`
|
||||
- `HtmlElement` can now properly handle array (i.e. `class`) and boolean (i.e. `checked`) attribute values
|
||||
- `HtmlElement` automatically flattens any attributes with array values into space-separated strings, removing duplicate entries
|
||||
- Combined separate classes/interfaces into one:
|
||||
- `DisallowedRawHtmlRenderer` replaces `DisallowedRawHtmlBlockRenderer` and `DisallowedRawHtmlInlineRenderer`
|
||||
- `NodeRendererInterface` replaces `BlockRendererInterface` and `InlineRendererInterface`
|
||||
- Renamed the following methods:
|
||||
- `Environment` and `ConfigurableEnvironmentInterface`:
|
||||
- `addBlockParser()` is now `addBlockStartParser()`
|
||||
- `ReferenceMap` and `ReferenceMapInterface`:
|
||||
- `addReference()` is now `add()`
|
||||
- `getReference()` is now `get()`
|
||||
- `listReferences()` is now `getIterator()`
|
||||
- Various node (block/inline) classes:
|
||||
- `getContent()` is now `getLiteral()`
|
||||
- `setContent()` is now `setLiteral()`
|
||||
- Moved and renamed the following constants:
|
||||
- `EnvironmentInterface::HTML_INPUT_ALLOW` is now `HtmlFilter::ALLOW`
|
||||
- `EnvironmentInterface::HTML_INPUT_ESCAPE` is now `HtmlFilter::ESCAPE`
|
||||
- `EnvironmentInterface::HTML_INPUT_STRIP` is now `HtmlFilter::STRIP`
|
||||
- `TableCell::TYPE_HEAD` is now `TableCell::TYPE_HEADER`
|
||||
- `TableCell::TYPE_BODY` is now `TableCell::TYPE_DATA`
|
||||
- Changed the visibility of the following properties:
|
||||
- `AttributesInline::$attributes` is now `private`
|
||||
- `AttributesInline::$block` is now `private`
|
||||
- `TableCell::$align` is now `private`
|
||||
- `TableCell::$type` is now `private`
|
||||
- `TableSection::$type` is now `private`
|
||||
- Several methods which previously returned `$this` now return `void`
|
||||
- `Delimiter::setPrevious()`
|
||||
- `Node::replaceChildren()`
|
||||
- `Context::setTip()`
|
||||
- `Context::setContainer()`
|
||||
- `Context::setBlocksParsed()`
|
||||
- `AbstractStringContainer::setContent()`
|
||||
- `AbstractWebResource::setUrl()`
|
||||
- Several classes are now marked `final`:
|
||||
- `ArrayCollection`
|
||||
- `Emphasis`
|
||||
- `FencedCode`
|
||||
- `Heading`
|
||||
- `HtmlBlock`
|
||||
- `HtmlElement`
|
||||
- `HtmlInline`
|
||||
- `IndentedCode`
|
||||
- `Newline`
|
||||
- `Strikethrough`
|
||||
- `Strong`
|
||||
- `Text`
|
||||
- `Heading` nodes no longer directly contain a copy of their inner text
|
||||
- `StringContainerInterface` can now be used for inlines, not just blocks
|
||||
- `ArrayCollection` only supports integer keys
|
||||
- `HtmlElement` now implements `Stringable`
|
||||
- `Cursor::saveState()` and `Cursor::restoreState()` now use `CursorState` objects instead of arrays
|
||||
- `NodeWalker::next()` now enters, traverses any children, and leaves all elements which may have children (basically all blocks plus any inlines with children). Previously, it only did this for elements explicitly marked as "containers".
|
||||
- `InvalidOptionException` was removed
|
||||
- Anything with a `getReference(): ReferenceInterface` method now implements `ReferencableInterface`
|
||||
- The `SmartPunct` extension now replaces all unpaired `Quote` elements with `Text` elements towards the end of parsing, making the `QuoteRenderer` unnecessary
|
||||
- Several changes made to the Footnote extension:
|
||||
- Footnote identifiers can no longer contain spaces
|
||||
- Anonymous footnotes can now span subsequent lines
|
||||
- Footnotes can now contain multiple lines of content, including sub-blocks, by indenting them
|
||||
- Footnote event listeners now have numbered priorities (but still execute in the same order)
|
||||
- Footnotes must now be separated from previous content by a blank line
|
||||
- The line numbers (keys) returned via `MarkdownInput::getLines()` now start at 1 instead of 0
|
||||
- `DelimiterProcessorCollectionInterface` now extends `Countable`
|
||||
- `RegexHelper::PARTIAL_` constants must always be used in case-insensitive contexts
|
||||
- `HeadingPermalinkProcessor` no longer accepts text normalizers via the constructor - these must be provided via configuration instead
|
||||
- Blocks which can't contain inlines will no longer be asked to render inlines
|
||||
- `AnonymousFootnoteRefParser` and `HeadingPermalinkProcessor` now implement `EnvironmentAwareInterface` instead of `ConfigurationAwareInterface`
|
||||
- The second argument to `TextNormalizerInterface::normalize()` must now be an array
|
||||
- The `title` attribute for `Link` and `Image` nodes is now stored using a dedicated property instead of stashing it in `$data`
|
||||
- `ListData::$delimiter` now returns either `ListBlock::DELIM_PERIOD` or `ListBlock::DELIM_PAREN` instead of the literal delimiter
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fixed parsing of footnotes without content**
|
||||
- **Fixed rendering of orphaned footnotes and footnote refs**
|
||||
- **Fixed some URL autolinks breaking too early** (#492)
|
||||
- Fixed `AbstractStringContainer` not actually being `abstract`
|
||||
|
||||
### Removed
|
||||
|
||||
- **Removed support for PHP 7.1, 7.2, and 7.3** (#625, #671)
|
||||
- **Removed all previously-deprecated functionality:**
|
||||
- Removed the ability to pass custom `Environment` instances into the `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` constructors
|
||||
- Removed the `Converter` class and `ConverterInterface`
|
||||
- Removed the `bin/commonmark` script
|
||||
- Removed the `Html5Entities` utility class
|
||||
- Removed the `InlineMentionParser` (use `MentionParser` instead)
|
||||
- Removed `DefaultSlugGenerator` and `SlugGeneratorInterface` from the `Extension/HeadingPermalink/Slug` sub-namespace (use the new ones under `./SlugGenerator` instead)
|
||||
- Removed the following `ArrayCollection` methods:
|
||||
- `add()`
|
||||
- `set()`
|
||||
- `get()`
|
||||
- `remove()`
|
||||
- `isEmpty()`
|
||||
- `contains()`
|
||||
- `indexOf()`
|
||||
- `containsKey()`
|
||||
- `replaceWith()`
|
||||
- `removeGaps()`
|
||||
- Removed the `ConfigurableEnvironmentInterface::setConfig()` method
|
||||
- Removed the `ListBlock::TYPE_UNORDERED` constant
|
||||
- Removed the `CommonMarkConverter::VERSION` constant
|
||||
- Removed the `HeadingPermalinkRenderer::DEFAULT_INNER_CONTENTS` constant
|
||||
- Removed the `heading_permalink/inner_contents` configuration option
|
||||
- **Removed now-unused classes:**
|
||||
- `AbstractStringContainerBlock`
|
||||
- `BlockRendererInterface`
|
||||
- `Context`
|
||||
- `ContextInterface`
|
||||
- `Converter`
|
||||
- `ConverterInterface`
|
||||
- `InlineRendererInterface`
|
||||
- `PunctuationParser` (was split into two classes: `DashParser` and `EllipsesParser`)
|
||||
- `QuoteRenderer`
|
||||
- `UnmatchedBlockCloser`
|
||||
- Removed the following methods, properties, and constants:
|
||||
- `AbstractBlock::$open`
|
||||
- `AbstractBlock::$lastLineBlank`
|
||||
- `AbstractBlock::isContainer()`
|
||||
- `AbstractBlock::canContain()`
|
||||
- `AbstractBlock::isCode()`
|
||||
- `AbstractBlock::matchesNextLine()`
|
||||
- `AbstractBlock::endsWithBlankLine()`
|
||||
- `AbstractBlock::setLastLineBlank()`
|
||||
- `AbstractBlock::shouldLastLineBeBlank()`
|
||||
- `AbstractBlock::isOpen()`
|
||||
- `AbstractBlock::finalize()`
|
||||
- `AbstractBlock::getData()`
|
||||
- `AbstractInline::getData()`
|
||||
- `ConfigurableEnvironmentInterface::addBlockParser()`
|
||||
- `ConfigurableEnvironmentInterface::mergeConfig()`
|
||||
- `Delimiter::setCanClose()`
|
||||
- `EnvironmentInterface::getConfig()`
|
||||
- `EnvironmentInterface::getInlineParsersForCharacter()`
|
||||
- `EnvironmentInterface::getInlineParserCharacterRegex()`
|
||||
- `HtmlRenderer::renderBlock()`
|
||||
- `HtmlRenderer::renderBlocks()`
|
||||
- `HtmlRenderer::renderInline()`
|
||||
- `HtmlRenderer::renderInlines()`
|
||||
- `Node::isContainer()`
|
||||
- `RegexHelper::matchAll()` (use the new `matchFirst()` method instead)
|
||||
- `RegexHelper::REGEX_WHITESPACE`
|
||||
- Removed the second `$contents` argument from the `Heading` constructor
|
||||
|
||||
### Deprecated
|
||||
|
||||
**The following things have been deprecated and will not be supported in v3.0:**
|
||||
|
||||
- `Environment::mergeConfig()` (set configuration before instantiation instead)
|
||||
- `Environment::createCommonMarkEnvironment()` and `Environment::createGFMEnvironment()`
|
||||
- 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.6.1...main
|
||||
[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
|
||||
[2.3.8]: https://github.com/thephpleague/commonmark/compare/2.3.7...2.3.8
|
||||
[2.3.7]: https://github.com/thephpleague/commonmark/compare/2.3.6...2.3.7
|
||||
[2.3.6]: https://github.com/thephpleague/commonmark/compare/2.3.5...2.3.6
|
||||
[2.3.5]: https://github.com/thephpleague/commonmark/compare/2.3.4...2.3.5
|
||||
[2.3.4]: https://github.com/thephpleague/commonmark/compare/2.3.3...2.3.4
|
||||
[2.3.3]: https://github.com/thephpleague/commonmark/compare/2.3.2...2.3.3
|
||||
[2.3.2]: https://github.com/thephpleague/commonmark/compare/2.3.2...main
|
||||
[2.3.1]: https://github.com/thephpleague/commonmark/compare/2.3.0...2.3.1
|
||||
[2.3.0]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.3.0
|
||||
[2.2.5]: https://github.com/thephpleague/commonmark/compare/2.2.4...2.2.5
|
||||
[2.2.4]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.2.4
|
||||
[2.2.3]: https://github.com/thephpleague/commonmark/compare/2.2.2...2.2.3
|
||||
[2.2.2]: https://github.com/thephpleague/commonmark/compare/2.2.1...2.2.2
|
||||
[2.2.1]: https://github.com/thephpleague/commonmark/compare/2.2.0...2.2.1
|
||||
[2.2.0]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.2.0
|
||||
[2.1.3]: https://github.com/thephpleague/commonmark/compare/2.1.2...2.1.3
|
||||
[2.1.2]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.1.2
|
||||
[2.1.1]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.1
|
||||
[2.1.0]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.0
|
||||
[2.0.4]: https://github.com/thephpleague/commonmark/compare/2.0.3...2.0.4
|
||||
[2.0.3]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.0.3
|
||||
[2.0.2]: https://github.com/thephpleague/commonmark/compare/2.0.1...2.0.2
|
||||
[2.0.1]: https://github.com/thephpleague/commonmark/compare/2.0.0...2.0.1
|
||||
[2.0.0]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc2...2.0.0
|
||||
[2.0.0-rc2]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc1...2.0.0-rc2
|
||||
[2.0.0-rc1]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta3...2.0.0-rc1
|
||||
[2.0.0-beta3]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta2...2.0.0-beta3
|
||||
[2.0.0-beta2]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta1...2.0.0-beta2
|
||||
[2.0.0-beta1]: https://github.com/thephpleague/commonmark/compare/1.6...2.0.0-beta1
|
||||
@@ -0,0 +1,223 @@
|
||||
# league/commonmark
|
||||
|
||||
[](https://packagist.org/packages/league/commonmark)
|
||||
[](https://packagist.org/packages/league/commonmark)
|
||||
[](LICENSE)
|
||||
[](https://github.com/thephpleague/commonmark/actions?query=workflow%3ATests+branch%3Amain)
|
||||
[](https://scrutinizer-ci.com/g/thephpleague/commonmark/code-structure)
|
||||
[](https://scrutinizer-ci.com/g/thephpleague/commonmark)
|
||||
[](https://shepherd.dev/github/thephpleague/commonmark)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/126)
|
||||
[](https://www.colinodell.com/sponsor)
|
||||
|
||||

|
||||
|
||||
**league/commonmark** is a highly-extensible PHP Markdown parser created by [Colin O'Dell][@colinodell] which supports the full [CommonMark] spec and [GitHub-Flavored Markdown]. It is based on the [CommonMark JS reference implementation][commonmark.js] by [John MacFarlane] \([@jgm]\).
|
||||
|
||||
## 📦 Installation & Basic Usage
|
||||
|
||||
This project requires PHP 7.4 or higher with the `mbstring` extension. To install it via [Composer] simply run:
|
||||
|
||||
``` bash
|
||||
$ composer require league/commonmark
|
||||
```
|
||||
|
||||
The `CommonMarkConverter` class provides a simple wrapper for converting CommonMark to HTML:
|
||||
|
||||
```php
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
echo $converter->convert('# Hello World!');
|
||||
|
||||
// <h1>Hello World!</h1>
|
||||
```
|
||||
|
||||
Or if you want GitHub-Flavored Markdown, use the `GithubFlavoredMarkdownConverter` class instead:
|
||||
|
||||
```php
|
||||
use League\CommonMark\GithubFlavoredMarkdownConverter;
|
||||
|
||||
$converter = new GithubFlavoredMarkdownConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
echo $converter->convert('# Hello World!');
|
||||
|
||||
// <h1>Hello World!</h1>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
> [!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
|
||||
|
||||
Full documentation on advanced usage, configuration, and customization can be found at [commonmark.thephpleague.com][docs].
|
||||
|
||||
## ⏫ Upgrading
|
||||
|
||||
Information on how to upgrade to newer versions of this library can be found at <https://commonmark.thephpleague.com/releases>.
|
||||
|
||||
## 💻 GitHub-Flavored Markdown
|
||||
|
||||
The `GithubFlavoredMarkdownConverter` shown earlier is a drop-in replacement for the `CommonMarkConverter` which adds additional features found in the GFM spec:
|
||||
|
||||
- Autolinks
|
||||
- Disallowed raw HTML
|
||||
- Strikethrough
|
||||
- Tables
|
||||
- Task Lists
|
||||
|
||||
See the [Extensions documentation](https://commonmark.thephpleague.com/customization/extensions/) for more details on how to include only certain GFM features if you don't want them all.
|
||||
|
||||
## 🗃️ Related Packages
|
||||
|
||||
### Integrations
|
||||
|
||||
- [CakePHP 3](https://github.com/gourmet/common-mark)
|
||||
- [Drupal](https://www.drupal.org/project/markdown)
|
||||
- [Laravel 4+](https://github.com/GrahamCampbell/Laravel-Markdown)
|
||||
- [Sculpin](https://github.com/bcremer/sculpin-commonmark-bundle)
|
||||
- [Symfony 2 & 3](https://github.com/webuni/commonmark-bundle)
|
||||
- [Symfony 4](https://github.com/avensome/commonmark-bundle)
|
||||
- [Twig Markdown extension](https://github.com/twigphp/markdown-extension)
|
||||
- [Twig filter and tag](https://github.com/aptoma/twig-markdown)
|
||||
- [Laravel CommonMark Blog](https://github.com/spekulatius/laravel-commonmark-blog)
|
||||
|
||||
### Included Extensions
|
||||
|
||||
See [our extension documentation](https://commonmark.thephpleague.com/extensions/overview) for a full list of extensions bundled with this library.
|
||||
|
||||
### Community Extensions
|
||||
|
||||
Custom parsers/renderers can be bundled into extensions which extend CommonMark. Here are some that you may find interesting:
|
||||
|
||||
- [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.
|
||||
- [Marker Extension](https://github.com/noah1400/commonmark-marker-extension) - Adds support of highlighted text (`<mark>` HTML tag)
|
||||
|
||||
Others can be found on [Packagist under the `commonmark-extension` package type](https://packagist.org/packages/league/commonmark?type=commonmark-extension).
|
||||
|
||||
If you build your own, feel free to submit a PR to add it to this list!
|
||||
|
||||
### Others
|
||||
|
||||
Check out the other cool things people are doing with `league/commonmark`: <https://packagist.org/packages/league/commonmark/dependents>
|
||||
|
||||
## 🏷️ Versioning
|
||||
|
||||
[SemVer](http://semver.org/) is followed closely. Minor and patch releases should not introduce breaking changes to the codebase; however, they might change the resulting AST or HTML output of parsed Markdown (due to bug fixes, spec changes, etc.) As a result, you might get slightly different HTML, but any custom code built onto this library should still function correctly.
|
||||
|
||||
Any classes or methods marked `@internal` are not intended for use outside of this library and are subject to breaking changes at any time, so please avoid using them.
|
||||
|
||||
## 🛠️ Maintenance & Support
|
||||
|
||||
When a new **minor** version (e.g. `2.0` -> `2.1`) is released, the previous one (`2.0`) will continue to receive security and critical bug fixes for *at least* 3 months.
|
||||
|
||||
When a new **major** version is released (e.g. `1.6` -> `2.0`), the previous one (`1.6`) will receive critical bug fixes for *at least* 3 months and security updates for 6 months after that new release comes out.
|
||||
|
||||
(This policy may change in the future and exceptions may be made on a case-by-case basis.)
|
||||
|
||||
**Professional support, including notification of new releases and security updates, is available through a [Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme).**
|
||||
|
||||
## 👷♀️ Contributing
|
||||
|
||||
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure with us.
|
||||
|
||||
If you encounter a bug in the spec, please report it to the [CommonMark] project. Any resulting fix will eventually be implemented in this project as well.
|
||||
|
||||
Contributions to this library are **welcome**, especially ones that:
|
||||
|
||||
* Improve usability or flexibility without compromising our ability to adhere to the [CommonMark spec]
|
||||
* Mirror fixes made to the [reference implementation][commonmark.js]
|
||||
* Optimize performance
|
||||
* Fix issues with adhering to the [CommonMark spec]
|
||||
|
||||
Major refactoring to core parsing logic should be avoided if possible so that we can easily follow updates made to [the reference implementation][commonmark.js]. That being said, we will absolutely consider changes which don't deviate too far from the reference spec or which are favored by other popular CommonMark implementations.
|
||||
|
||||
Please see [CONTRIBUTING](https://github.com/thephpleague/commonmark/blob/main/.github/CONTRIBUTING.md) for additional details.
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
``` bash
|
||||
$ composer test
|
||||
```
|
||||
|
||||
This will also test league/commonmark against the latest supported spec.
|
||||
|
||||
## 🚀 Performance Benchmarks
|
||||
|
||||
You can compare the performance of **league/commonmark** to other popular parsers by running the included benchmark tool:
|
||||
|
||||
``` bash
|
||||
$ ./tests/benchmark/benchmark.php
|
||||
```
|
||||
|
||||
## 👥 Credits & Acknowledgements
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
We'd also like to extend our sincere thanks the following sponsors who support ongoing development of this project:
|
||||
|
||||
- [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
|
||||
|
||||
Are you interested in sponsoring development of this project? See <https://www.colinodell.com/sponsor> for a list of ways to contribute.
|
||||
|
||||
## 📄 License
|
||||
|
||||
**league/commonmark** is licensed under the BSD-3 license. See the [`LICENSE`](LICENSE) file for more details.
|
||||
|
||||
## 🏛️ Governance
|
||||
|
||||
This project is primarily maintained by [Colin O'Dell][@colinodell]. Members of the [PHP League] Leadership Team may occasionally assist with some of these duties.
|
||||
|
||||
## 🗺️ Who Uses It?
|
||||
|
||||
This project is used by [Drupal](https://www.drupal.org/project/markdown), [Laravel Framework](https://laravel.com/), [Cachet](https://cachethq.io/), [Firefly III](https://firefly-iii.org/), [Neos](https://www.neos.io/), [Daux.io](https://daux.io/), and [more](https://packagist.org/packages/league/commonmark/dependents)!
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<b>
|
||||
<a href="https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme">Get professional support for league/commonmark with a Tidelift subscription</a>
|
||||
</b>
|
||||
<br>
|
||||
<sub>
|
||||
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
|
||||
</sub>
|
||||
</div>
|
||||
|
||||
[CommonMark]: http://commonmark.org/
|
||||
[CommonMark spec]: http://spec.commonmark.org/
|
||||
[commonmark.js]: https://github.com/jgm/commonmark.js
|
||||
[GitHub-Flavored Markdown]: https://github.github.com/gfm/
|
||||
[John MacFarlane]: http://johnmacfarlane.net
|
||||
[docs]: https://commonmark.thephpleague.com/
|
||||
[docs-examples]: https://commonmark.thephpleague.com/customization/overview/#examples
|
||||
[docs-example-twitter]: https://commonmark.thephpleague.com/customization/inline-parsing#example-1---twitter-handles
|
||||
[docs-example-smilies]: https://commonmark.thephpleague.com/customization/inline-parsing#example-2---emoticons
|
||||
[All Contributors]: https://github.com/thephpleague/commonmark/contributors
|
||||
[@colinodell]: https://www.twitter.com/colinodell
|
||||
[@jgm]: https://github.com/jgm
|
||||
[jgm/stmd]: https://github.com/jgm/stmd
|
||||
[Composer]: https://getcomposer.org/
|
||||
[PHP League]: https://thephpleague.com
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"name": "league/commonmark",
|
||||
"type": "library",
|
||||
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
|
||||
"keywords": ["markdown","parser","commonmark","gfm","github","flavored","github-flavored","md"],
|
||||
"homepage": "https://commonmark.thephpleague.com",
|
||||
"license": "BSD-3-Clause",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Colin O'Dell",
|
||||
"email": "colinodell@gmail.com",
|
||||
"homepage": "https://www.colinodell.com",
|
||||
"role": "Lead Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://commonmark.thephpleague.com/",
|
||||
"forum": "https://github.com/thephpleague/commonmark/discussions",
|
||||
"issues": "https://github.com/thephpleague/commonmark/issues",
|
||||
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
|
||||
"source": "https://github.com/thephpleague/commonmark"
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"ext-mbstring": "*",
|
||||
"league/config": "^1.1.1",
|
||||
"psr/event-dispatcher": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.1 || ^3.0",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"cebe/markdown": "^1.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",
|
||||
"github/gfm": "0.29.0",
|
||||
"michelf/php-markdown": "^1.4 || ^2.0",
|
||||
"nyholm/psr7": "^1.5",
|
||||
"phpstan/phpstan": "^1.8.2",
|
||||
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
|
||||
"scrutinizer/ocular": "^1.8.1",
|
||||
"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"
|
||||
},
|
||||
"minimum-stability": "beta",
|
||||
"suggest": {
|
||||
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "commonmark/commonmark.js",
|
||||
"version": "0.31.1",
|
||||
"dist": {
|
||||
"url": "https://github.com/commonmark/commonmark.js/archive/0.31.1.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "commonmark/cmark",
|
||||
"version": "0.31.1",
|
||||
"dist": {
|
||||
"url": "https://github.com/commonmark/cmark/archive/0.31.1.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "github/gfm",
|
||||
"version": "0.29.0",
|
||||
"dist": {
|
||||
"url": "https://github.com/github/cmark-gfm/archive/0.29.0.gfm.13.zip",
|
||||
"type": "zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\CommonMark\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"League\\CommonMark\\Tests\\Unit\\": "tests/unit",
|
||||
"League\\CommonMark\\Tests\\Functional\\": "tests/functional",
|
||||
"League\\CommonMark\\Tests\\PHPStan\\": "tests/phpstan"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs",
|
||||
"phpstan": "phpstan analyse",
|
||||
"phpunit": "phpunit --no-coverage",
|
||||
"psalm": "psalm --stats",
|
||||
"pathological": "tests/pathological/test.php",
|
||||
"test": [
|
||||
"@phpcs",
|
||||
"@phpstan",
|
||||
"@psalm",
|
||||
"@phpunit",
|
||||
"@pathological"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* 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\Inline\AbstractStringContainer;
|
||||
|
||||
interface DelimiterInterface
|
||||
{
|
||||
public function canClose(): bool;
|
||||
|
||||
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;
|
||||
|
||||
public function getIndex(): ?int;
|
||||
|
||||
public function getNext(): ?DelimiterInterface;
|
||||
|
||||
public function setNext(?DelimiterInterface $next): void;
|
||||
|
||||
public function getLength(): int;
|
||||
|
||||
public function setLength(int $length): void;
|
||||
|
||||
public function getOriginalLength(): int;
|
||||
|
||||
public function getInlineNode(): AbstractStringContainer;
|
||||
|
||||
public function getPrevious(): ?DelimiterInterface;
|
||||
|
||||
public function setPrevious(?DelimiterInterface $previous): void;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Delimiter;
|
||||
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
/**
|
||||
* Delimiter parsing is implemented as an Inline Parser with the lowest-possible priority
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DelimiterParser implements InlineParserInterface
|
||||
{
|
||||
private DelimiterProcessorCollection $collection;
|
||||
|
||||
public function __construct(DelimiterProcessorCollection $collection)
|
||||
{
|
||||
$this->collection = $collection;
|
||||
}
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::oneOf(...$this->collection->getDelimiterCharacters());
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$character = $inlineContext->getFullMatch();
|
||||
$numDelims = 0;
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$processor = $this->collection->getDelimiterProcessor($character);
|
||||
|
||||
\assert($processor !== null); // Delimiter processor should never be null here
|
||||
|
||||
$charBefore = $cursor->peek(-1);
|
||||
if ($charBefore === null) {
|
||||
$charBefore = "\n";
|
||||
}
|
||||
|
||||
while ($cursor->peek($numDelims) === $character) {
|
||||
++$numDelims;
|
||||
}
|
||||
|
||||
if ($numDelims < $processor->getMinLength()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cursor->advanceBy($numDelims);
|
||||
|
||||
$charAfter = $cursor->getCurrentCharacter();
|
||||
if ($charAfter === null) {
|
||||
$charAfter = "\n";
|
||||
}
|
||||
|
||||
[$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
|
||||
$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose, $inlineContext->getCursor()->getPosition());
|
||||
$inlineContext->getDelimiterStack()->push($delimiter);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool[]
|
||||
*/
|
||||
private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array
|
||||
{
|
||||
$afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
|
||||
$afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
|
||||
$beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
|
||||
$beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
|
||||
|
||||
$leftFlanking = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
|
||||
$rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
|
||||
|
||||
if ($character === '_') {
|
||||
$canOpen = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation);
|
||||
$canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation);
|
||||
} else {
|
||||
$canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
|
||||
$canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
|
||||
}
|
||||
|
||||
return [$canOpen, $canClose];
|
||||
}
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
|
||||
* - (c) Atlassian Pty Ltd
|
||||
*
|
||||
* 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\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) {
|
||||
$this->top->setNext($newDelimiter);
|
||||
}
|
||||
|
||||
$this->top = $newDelimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addBracket(Node $node, int $index, bool $image): void
|
||||
{
|
||||
if ($this->brackets !== null) {
|
||||
$this->brackets->setHasNext(true);
|
||||
}
|
||||
|
||||
$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
|
||||
{
|
||||
if ($delimiter->getPrevious() !== null) {
|
||||
/** @psalm-suppress PossiblyNullReference */
|
||||
$delimiter->getPrevious()->setNext($delimiter->getNext());
|
||||
}
|
||||
|
||||
if ($delimiter->getNext() === null) {
|
||||
// top of stack
|
||||
$this->top = $delimiter->getPrevious();
|
||||
} else {
|
||||
/** @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
|
||||
{
|
||||
$delimiter->getInlineNode()->detach();
|
||||
$this->removeDelimiter($delimiter);
|
||||
}
|
||||
|
||||
private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
|
||||
{
|
||||
$delimiter = $closer->getPrevious();
|
||||
$openerPosition = self::getIndex($opener);
|
||||
while ($delimiter !== null && self::getIndex($delimiter) > $openerPosition) {
|
||||
$previous = $delimiter->getPrevious();
|
||||
$this->removeDelimiter($delimiter);
|
||||
$delimiter = $previous;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DelimiterInterface|int|null $stackBottom
|
||||
*/
|
||||
public function removeAll($stackBottom = null): void
|
||||
{
|
||||
$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;
|
||||
while ($opener !== null) {
|
||||
if ($opener->getChar() === $character) {
|
||||
$opener->setActive(false);
|
||||
}
|
||||
|
||||
$opener = $opener->getPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
if (! \is_array($characters)) {
|
||||
$characters = [$characters];
|
||||
}
|
||||
|
||||
$opener = $this->top;
|
||||
while ($opener !== null) {
|
||||
if (\in_array($opener->getChar(), $characters, true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$opener = $opener->getPrevious();
|
||||
}
|
||||
|
||||
return $opener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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($stackBottomPosition);
|
||||
|
||||
// Move forward, looking for closers, and handling each
|
||||
while ($closer !== null) {
|
||||
$closingDelimiterChar = $closer->getChar();
|
||||
|
||||
$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 && ($openerPosition = self::getIndex($opener)) > $stackBottomPosition && $openerPosition >= ($openersBottom[$openersBottomCacheKey] ?? 0)) {
|
||||
if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
|
||||
$potentialOpenerFound = true;
|
||||
$useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
|
||||
if ($useDelims > 0) {
|
||||
$openerFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$opener = $opener->getPrevious();
|
||||
}
|
||||
|
||||
if (! $openerFound) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
\assert($opener !== null);
|
||||
|
||||
$openerNode = $opener->getInlineNode();
|
||||
$closerNode = $closer->getInlineNode();
|
||||
|
||||
// Remove number of used delimiters from stack and inline nodes.
|
||||
$opener->setLength($opener->getLength() - $useDelims);
|
||||
$closer->setLength($closer->getLength() - $useDelims);
|
||||
|
||||
$openerNode->setLiteral(\substr($openerNode->getLiteral(), 0, -$useDelims));
|
||||
$closerNode->setLiteral(\substr($closerNode->getLiteral(), 0, -$useDelims));
|
||||
|
||||
$this->removeDelimitersBetween($opener, $closer);
|
||||
// The delimiter processor can re-parent the nodes between opener and closer,
|
||||
// so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
|
||||
AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
|
||||
$delimiterProcessor->process($openerNode, $closerNode, $useDelims);
|
||||
|
||||
// No delimiter characters left to process, so we can remove delimiter and the now empty node.
|
||||
if ($opener->getLength() === 0) {
|
||||
$this->removeDelimiterAndNode($opener);
|
||||
}
|
||||
|
||||
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
|
||||
if ($closer->getLength() === 0) {
|
||||
$next = $closer->getNext();
|
||||
$this->removeDelimiterAndNode($closer);
|
||||
$closer = $next;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all delimiters
|
||||
$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;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
|
||||
* - (c) Atlassian Pty Ltd
|
||||
*
|
||||
* 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;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
/**
|
||||
* Interface for a delimiter processor
|
||||
*/
|
||||
interface DelimiterProcessorInterface
|
||||
{
|
||||
/**
|
||||
* Returns the character that marks the beginning of a delimited node.
|
||||
*
|
||||
* This must not clash with any other processors being added to the environment.
|
||||
*/
|
||||
public function getOpeningCharacter(): string;
|
||||
|
||||
/**
|
||||
* Returns the character that marks the ending of a delimited node.
|
||||
*
|
||||
* This must not clash with any other processors being added to the environment.
|
||||
*
|
||||
* Note that for a symmetric delimiter such as "*", this is the same as the opening.
|
||||
*/
|
||||
public function getClosingCharacter(): string;
|
||||
|
||||
/**
|
||||
* Minimum number of delimiter characters that are needed to active this.
|
||||
*
|
||||
* Must be at least 1.
|
||||
*/
|
||||
public function getMinLength(): int;
|
||||
|
||||
/**
|
||||
* Determine how many (if any) of the delimiter characters should be used.
|
||||
*
|
||||
* This allows implementations to decide how many characters to be used
|
||||
* based on the properties of the delimiter runs. An implementation can also
|
||||
* 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
|
||||
*/
|
||||
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int;
|
||||
|
||||
/**
|
||||
* Process the matched delimiters, e.g. by wrapping the nodes between opener
|
||||
* and closer in a new node, or appending a new node after the opener.
|
||||
*
|
||||
* Note that removal of the delimiter from the delimiter nodes and detaching
|
||||
* them is done by the caller.
|
||||
*
|
||||
* @param AbstractStringContainer $opener The node that contained the opening delimiter
|
||||
* @param AbstractStringContainer $closer The node that contained the closing delimiter
|
||||
* @param int $delimiterUse The number of delimiters that were used
|
||||
*/
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void;
|
||||
}
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Environment;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterParser;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
|
||||
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Event\ListenerData;
|
||||
use League\CommonMark\Exception\AlreadyInitializedException;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Extension\ExtensionInterface;
|
||||
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
|
||||
use League\CommonMark\Normalizer\SlugNormalizer;
|
||||
use League\CommonMark\Normalizer\TextNormalizerInterface;
|
||||
use League\CommonMark\Normalizer\UniqueSlugNormalizer;
|
||||
use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Renderer\NodeRendererInterface;
|
||||
use League\CommonMark\Util\HtmlFilter;
|
||||
use League\CommonMark\Util\PrioritizedList;
|
||||
use League\Config\Configuration;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
use Nette\Schema\Expect;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\EventDispatcher\ListenerProviderInterface;
|
||||
use Psr\EventDispatcher\StoppableEventInterface;
|
||||
|
||||
final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var ExtensionInterface[]
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $extensions = [];
|
||||
|
||||
/**
|
||||
* @var ExtensionInterface[]
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $uninitializedExtensions = [];
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private bool $extensionsInitialized = false;
|
||||
|
||||
/**
|
||||
* @var PrioritizedList<BlockStartParserInterface>
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private PrioritizedList $blockStartParsers;
|
||||
|
||||
/**
|
||||
* @var PrioritizedList<InlineParserInterface>
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private PrioritizedList $inlineParsers;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private DelimiterProcessorCollection $delimiterProcessors;
|
||||
|
||||
/**
|
||||
* @var array<string, PrioritizedList<NodeRendererInterface>>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $renderersByClass = [];
|
||||
|
||||
/**
|
||||
* @var PrioritizedList<ListenerData>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private PrioritizedList $listenerData;
|
||||
|
||||
private ?EventDispatcherInterface $eventDispatcher = null;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private Configuration $config;
|
||||
|
||||
private ?TextNormalizerInterface $slugNormalizer = null;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = self::createDefaultConfiguration();
|
||||
$this->config->merge($config);
|
||||
|
||||
$this->blockStartParsers = new PrioritizedList();
|
||||
$this->inlineParsers = new PrioritizedList();
|
||||
$this->listenerData = new PrioritizedList();
|
||||
$this->delimiterProcessors = new DelimiterProcessorCollection();
|
||||
|
||||
// Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter
|
||||
// and is therefore unlikely to match any lines as a block start.
|
||||
$this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249);
|
||||
}
|
||||
|
||||
public function getConfiguration(): ConfigurationInterface
|
||||
{
|
||||
return $this->config->reader();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public function mergeConfig(array $config): void
|
||||
{
|
||||
@\trigger_error('Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.', \E_USER_DEPRECATED);
|
||||
|
||||
$this->assertUninitialized('Failed to modify configuration.');
|
||||
|
||||
$this->config->merge($config);
|
||||
}
|
||||
|
||||
public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add block start parser.');
|
||||
|
||||
$this->blockStartParsers->add($parser, $priority);
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add inline parser.');
|
||||
|
||||
$this->inlineParsers->add($parser, $priority);
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add delimiter processor.');
|
||||
$this->delimiterProcessors->add($processor);
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($processor);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add renderer.');
|
||||
|
||||
if (! isset($this->renderersByClass[$nodeClass])) {
|
||||
$this->renderersByClass[$nodeClass] = new PrioritizedList();
|
||||
}
|
||||
|
||||
$this->renderersByClass[$nodeClass]->add($renderer, $priority);
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($renderer);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBlockStartParsers(): iterable
|
||||
{
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
return $this->blockStartParsers->getIterator();
|
||||
}
|
||||
|
||||
public function getDelimiterProcessors(): DelimiterProcessorCollection
|
||||
{
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
return $this->delimiterProcessors;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRenderersForClass(string $nodeClass): iterable
|
||||
{
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
// If renderers are defined for this specific class, return them immediately
|
||||
if (isset($this->renderersByClass[$nodeClass])) {
|
||||
return $this->renderersByClass[$nodeClass];
|
||||
}
|
||||
|
||||
/** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
|
||||
while (\class_exists($parent ??= $nodeClass) && $parent = \get_parent_class($parent)) {
|
||||
if (! isset($this->renderersByClass[$parent])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// "Cache" this result to avoid future loops
|
||||
return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getExtensions(): iterable
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single extension
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add extension.');
|
||||
|
||||
$this->extensions[] = $extension;
|
||||
$this->uninitializedExtensions[] = $extension;
|
||||
|
||||
if ($extension instanceof ConfigurableExtensionInterface) {
|
||||
$extension->configureSchema($this->config);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function initializeExtensions(): void
|
||||
{
|
||||
// Initialize the slug normalizer
|
||||
$this->getSlugNormalizer();
|
||||
|
||||
// Ask all extensions to register their components
|
||||
while (\count($this->uninitializedExtensions) > 0) {
|
||||
foreach ($this->uninitializedExtensions as $i => $extension) {
|
||||
$extension->register($this);
|
||||
unset($this->uninitializedExtensions[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->extensionsInitialized = true;
|
||||
|
||||
// Create the special delimiter parser if any processors were registered
|
||||
if ($this->delimiterProcessors->count() > 0) {
|
||||
$this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
|
||||
}
|
||||
}
|
||||
|
||||
private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
|
||||
{
|
||||
if ($object instanceof EnvironmentAwareInterface) {
|
||||
$object->setEnvironment($this);
|
||||
}
|
||||
|
||||
if ($object instanceof ConfigurationAwareInterface) {
|
||||
$object->setConfiguration($this->config->reader());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Instantiate the environment and add the extension yourself
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function createCommonMarkEnvironment(array $config = []): Environment
|
||||
{
|
||||
$environment = new self($config);
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Instantiate the environment and add the extension yourself
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function createGFMEnvironment(array $config = []): Environment
|
||||
{
|
||||
$environment = new self($config);
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new GithubFlavoredMarkdownExtension());
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
|
||||
{
|
||||
$this->assertUninitialized('Failed to add event listener.');
|
||||
|
||||
$this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
|
||||
|
||||
if (\is_object($listener)) {
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($listener);
|
||||
} elseif (\is_array($listener) && \is_object($listener[0])) {
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dispatch(object $event): object
|
||||
{
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
if ($this->eventDispatcher !== null) {
|
||||
return $this->eventDispatcher->dispatch($event);
|
||||
}
|
||||
|
||||
foreach ($this->getListenersForEvent($event) as $listener) {
|
||||
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
|
||||
return $event;
|
||||
}
|
||||
|
||||
$listener($event);
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return iterable<callable>
|
||||
*/
|
||||
public function getListenersForEvent(object $event): iterable
|
||||
{
|
||||
foreach ($this->listenerData as $listenerData) {
|
||||
\assert($listenerData instanceof ListenerData);
|
||||
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
if (! \is_a($event, $listenerData->getEvent())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield function (object $event) use ($listenerData) {
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
return \call_user_func($listenerData->getListener(), $event);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<InlineParserInterface>
|
||||
*/
|
||||
public function getInlineParsers(): iterable
|
||||
{
|
||||
if (! $this->extensionsInitialized) {
|
||||
$this->initializeExtensions();
|
||||
}
|
||||
|
||||
return $this->inlineParsers->getIterator();
|
||||
}
|
||||
|
||||
public function getSlugNormalizer(): TextNormalizerInterface
|
||||
{
|
||||
if ($this->slugNormalizer === null) {
|
||||
$normalizer = $this->config->get('slug_normalizer/instance');
|
||||
\assert($normalizer instanceof TextNormalizerInterface);
|
||||
$this->injectEnvironmentAndConfigurationIfNeeded($normalizer);
|
||||
|
||||
if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) {
|
||||
$normalizer = new UniqueSlugNormalizer($normalizer);
|
||||
}
|
||||
|
||||
if ($normalizer instanceof UniqueSlugNormalizer) {
|
||||
if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) {
|
||||
$this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000);
|
||||
}
|
||||
}
|
||||
|
||||
$this->slugNormalizer = $normalizer;
|
||||
}
|
||||
|
||||
return $this->slugNormalizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AlreadyInitializedException
|
||||
*/
|
||||
private function assertUninitialized(string $message): void
|
||||
{
|
||||
if ($this->extensionsInitialized) {
|
||||
throw new AlreadyInitializedException($message . ' Extensions have already been initialized.');
|
||||
}
|
||||
}
|
||||
|
||||
public static function createDefaultConfiguration(): Configuration
|
||||
{
|
||||
return new Configuration([
|
||||
'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"),
|
||||
'soft_break' => Expect::string("\n"),
|
||||
]),
|
||||
'slug_normalizer' => Expect::structure([
|
||||
'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()),
|
||||
'max_length' => Expect::int()->min(0)->default(255),
|
||||
'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Util;
|
||||
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AttributesHelper
|
||||
{
|
||||
private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
|
||||
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parseAttributes(Cursor $cursor): array
|
||||
{
|
||||
$state = $cursor->saveState();
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
|
||||
// Quick check to see if we might have attributes
|
||||
if ($cursor->getCharacter() !== '{') {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Attempt to match the entire attribute list expression
|
||||
// While this is less performant than checking for '{' now and '}' later, it simplifies
|
||||
// matching individual attributes since they won't need to look ahead for the closing '}'
|
||||
// while dealing with the fact that attributes can technically contain curly braces.
|
||||
// So we'll just match the start and end braces up front.
|
||||
$attributeExpression = $cursor->match(self::ATTRIBUTE_LIST);
|
||||
if ($attributeExpression === null) {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trim the leading '{' or '{:' and the trailing '}'
|
||||
$attributeExpression = \ltrim(\substr($attributeExpression, 1, -1), ':');
|
||||
$attributeCursor = new Cursor($attributeExpression);
|
||||
|
||||
/** @var array<string, mixed> $attributes */
|
||||
$attributes = [];
|
||||
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) {
|
||||
if ($attribute[0] === '#') {
|
||||
$attributes['id'] = \substr($attribute, 1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($attribute[0] === '.') {
|
||||
$attributes['class'][] = \substr($attribute, 1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @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) {
|
||||
$value = \substr($value, 1, -1);
|
||||
}
|
||||
|
||||
if (\strtolower(\trim($name)) === 'class') {
|
||||
foreach (\array_filter(\explode(' ', \trim($value))) as $class) {
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
} else {
|
||||
$attributes[\trim($name)] = \trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($attributes['class'])) {
|
||||
$attributes['class'] = \implode(' ', (array) $attributes['class']);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node|array<string, mixed> $attributes1
|
||||
* @param Node|array<string, mixed> $attributes2
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function mergeAttributes($attributes1, $attributes2): array
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ([$attributes1, $attributes2] as $arg) {
|
||||
if ($arg instanceof Node) {
|
||||
$arg = $arg->data->get('attributes');
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $arg */
|
||||
$arg = (array) $arg;
|
||||
if (isset($arg['class'])) {
|
||||
if (\is_string($arg['class'])) {
|
||||
$arg['class'] = \array_filter(\explode(' ', \trim($arg['class'])));
|
||||
}
|
||||
|
||||
foreach ($arg['class'] as $class) {
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
|
||||
unset($arg['class']);
|
||||
}
|
||||
|
||||
$attributes = \array_merge($attributes, $arg);
|
||||
}
|
||||
|
||||
if (isset($attributes['class'])) {
|
||||
$attributes['class'] = \implode(' ', $attributes['class']);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
<?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\Extension\Autolink;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
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->getConfiguration()->get('autolink.allowed_protocols'),
|
||||
$environment->getConfiguration()->get('autolink.default_protocol'),
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+157
@@ -0,0 +1,157 @@
|
||||
<?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\Extension\Autolink;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class UrlAutolinkParser implements InlineParserInterface
|
||||
{
|
||||
private const ALLOWED_AFTER = [null, ' ', "\t", "\n", "\x0b", "\x0c", "\x0d", '*', '_', '~', '('];
|
||||
|
||||
// RegEx adapted from https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Validator/Constraints/UrlValidator.php
|
||||
private const REGEX = '~
|
||||
(
|
||||
# Must start with a supported scheme + auth, or "www"
|
||||
(?:
|
||||
(?:%s):// # protocol
|
||||
(?:(?:(?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth
|
||||
|www\.)
|
||||
(?:
|
||||
(?:
|
||||
(?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode
|
||||
|
|
||||
(?:[\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
|
||||
)\.?
|
||||
| # or
|
||||
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address
|
||||
| # or
|
||||
\[
|
||||
(?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
|
||||
\] # an IPv6 address
|
||||
)
|
||||
(?::[0-9]+)? # a port (optional)
|
||||
(?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path
|
||||
(?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional)
|
||||
(?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional)
|
||||
)~ixu';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private array $prefixes = ['www.'];
|
||||
|
||||
/**
|
||||
* @psalm-var non-empty-string
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private string $finalRegex;
|
||||
|
||||
private string $defaultProtocol;
|
||||
|
||||
/**
|
||||
* @param array<int, string> $allowedProtocols
|
||||
*/
|
||||
public function __construct(array $allowedProtocols = ['http', 'https', 'ftp'], string $defaultProtocol = 'http')
|
||||
{
|
||||
/**
|
||||
* @psalm-suppress PropertyTypeCoercion
|
||||
*/
|
||||
$this->finalRegex = \sprintf(self::REGEX, \implode('|', $allowedProtocols));
|
||||
|
||||
foreach ($allowedProtocols as $protocol) {
|
||||
$this->prefixes[] = $protocol . '://';
|
||||
}
|
||||
|
||||
$this->defaultProtocol = $defaultProtocol;
|
||||
}
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::oneOf(...$this->prefixes);
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$cursor = $inlineContext->getCursor();
|
||||
|
||||
// Autolinks can only come at the beginning of a line, after whitespace, or certain delimiting characters
|
||||
$previousChar = $cursor->peek(-1);
|
||||
if (! \in_array($previousChar, self::ALLOWED_AFTER, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we have a valid URL
|
||||
if (! \preg_match($this->finalRegex, $cursor->getRemainder(), $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $matches[0];
|
||||
|
||||
// Does the URL end with punctuation that should be stripped?
|
||||
if (\preg_match('/(.+?)([?!.,:*_~]+)$/', $url, $matches)) {
|
||||
// Add the punctuation later
|
||||
$url = $matches[1];
|
||||
}
|
||||
|
||||
// Does the URL end with something that looks like an entity reference?
|
||||
if (\preg_match('/(.+)(&[A-Za-z0-9]+;)$/', $url, $matches)) {
|
||||
$url = $matches[1];
|
||||
}
|
||||
|
||||
// Does the URL need unmatched parens chopped off?
|
||||
if (\substr($url, -1) === ')' && ($diff = self::diffParens($url)) > 0) {
|
||||
$url = \substr($url, 0, -$diff);
|
||||
}
|
||||
|
||||
$cursor->advanceBy(\mb_strlen($url, 'UTF-8'));
|
||||
|
||||
// Auto-prefix 'http(s)://' onto 'www' URLs
|
||||
if (\substr($url, 0, 4) === 'www.') {
|
||||
$inlineContext->getContainer()->appendChild(new Link($this->defaultProtocol . '://' . $url, $url));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$inlineContext->getContainer()->appendChild(new Link($url, $url));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function diffParens(string $content): int
|
||||
{
|
||||
// Scan the entire autolink for the total number of parentheses.
|
||||
// If there is a greater number of closing parentheses than opening ones,
|
||||
// we don’t consider ANY of the last characters as part of the autolink,
|
||||
// in order to facilitate including an autolink inside a parenthesis.
|
||||
\preg_match_all('/[()]/', $content, $matches);
|
||||
|
||||
$charCount = ['(' => 0, ')' => 0];
|
||||
foreach ($matches[0] as $char) {
|
||||
$charCount[$char]++;
|
||||
}
|
||||
|
||||
return $charCount[')'] - $charCount['('];
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
|
||||
* - (c) Atlassian Pty Ltd
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Delimiter\Processor;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
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 CacheableDelimiterProcessorInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private string $char;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ConfigurationInterface $config;
|
||||
|
||||
/**
|
||||
* @param string $char The emphasis character to use (typically '*' or '_')
|
||||
*/
|
||||
public function __construct(string $char)
|
||||
{
|
||||
$this->char = $char;
|
||||
}
|
||||
|
||||
public function getOpeningCharacter(): string
|
||||
{
|
||||
return $this->char;
|
||||
}
|
||||
|
||||
public function getClosingCharacter(): string
|
||||
{
|
||||
return $this->char;
|
||||
}
|
||||
|
||||
public function getMinLength(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
|
||||
{
|
||||
// "Multiple of 3" rule for internal delimiter runs
|
||||
if (($opener->canClose() || $closer->canOpen()) && $closer->getOriginalLength() % 3 !== 0 && ($opener->getOriginalLength() + $closer->getOriginalLength()) % 3 === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate actual number of delimiters used from this closer
|
||||
if ($opener->getLength() >= 2 && $closer->getLength() >= 2) {
|
||||
if ($this->config->get('commonmark/enable_strong')) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->config->get('commonmark/enable_em')) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
|
||||
{
|
||||
if ($delimiterUse === 1) {
|
||||
$emphasis = new Emphasis($this->char);
|
||||
} elseif ($delimiterUse === 2) {
|
||||
$emphasis = new Strong($this->char . $this->char);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$next = $opener->next();
|
||||
while ($next !== null && $next !== $closer) {
|
||||
$tmp = $next->next();
|
||||
$emphasis->appendChild($next);
|
||||
$next = $tmp;
|
||||
}
|
||||
|
||||
$opener->insertAfter($emphasis);
|
||||
}
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Node\Block;
|
||||
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Node\Block\TightBlockInterface;
|
||||
|
||||
class ListBlock extends AbstractBlock implements TightBlockInterface
|
||||
{
|
||||
public const TYPE_BULLET = 'bullet';
|
||||
public const TYPE_ORDERED = 'ordered';
|
||||
|
||||
public const DELIM_PERIOD = 'period';
|
||||
public const DELIM_PAREN = 'paren';
|
||||
|
||||
protected bool $tight = false; // TODO Make lists tight by default in v3
|
||||
|
||||
/** @psalm-readonly */
|
||||
protected ListData $listData;
|
||||
|
||||
public function __construct(ListData $listData)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->listData = $listData;
|
||||
}
|
||||
|
||||
public function getListData(): ListData
|
||||
{
|
||||
return $this->listData;
|
||||
}
|
||||
|
||||
public function isTight(): bool
|
||||
{
|
||||
return $this->tight;
|
||||
}
|
||||
|
||||
public function setTight(bool $tight): void
|
||||
{
|
||||
$this->tight = $tight;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?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\Extension\CommonMark\Parser\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Util\ArrayCollection;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
final class FencedCodeParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private FencedCode $block;
|
||||
|
||||
/** @var ArrayCollection<string> */
|
||||
private ArrayCollection $strings;
|
||||
|
||||
public function __construct(int $fenceLength, string $fenceChar, int $fenceOffset)
|
||||
{
|
||||
$this->block = new FencedCode($fenceLength, $fenceChar, $fenceOffset);
|
||||
$this->strings = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getBlock(): FencedCode
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
// Check for closing code fence
|
||||
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === $this->block->getChar()) {
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
// Skip optional spaces of fence offset
|
||||
// Optimization: don't attempt to match if we're at a non-space position
|
||||
if ($cursor->getNextNonSpacePosition() > $cursor->getPosition()) {
|
||||
$cursor->match('/^ {0,' . $this->block->getOffset() . '}/');
|
||||
}
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function addLine(string $line): void
|
||||
{
|
||||
$this->strings[] = $line;
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
// first line becomes info string
|
||||
$firstLine = $this->strings->first();
|
||||
if ($firstLine === false) {
|
||||
$firstLine = '';
|
||||
}
|
||||
|
||||
$this->block->setInfo(RegexHelper::unescape(\trim($firstLine)));
|
||||
|
||||
if ($this->strings->count() === 1) {
|
||||
$this->block->setLiteral('');
|
||||
} else {
|
||||
$this->block->setLiteral(\implode("\n", $this->strings->slice(1)) . "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?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\Extension\CommonMark\Parser\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Util\ArrayCollection;
|
||||
|
||||
final class IndentedCodeParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private IndentedCode $block;
|
||||
|
||||
/** @var ArrayCollection<string> */
|
||||
private ArrayCollection $strings;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->block = new IndentedCode();
|
||||
$this->strings = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getBlock(): IndentedCode
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if ($cursor->isIndented()) {
|
||||
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
if ($cursor->isBlank()) {
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
public function addLine(string $line): void
|
||||
{
|
||||
$this->strings[] = $line;
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
$lines = $this->strings->toArray();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
$this->block->setLiteral(\implode("\n", $lines) . "\n");
|
||||
$this->block->setEndLine($this->block->getStartLine() + \count($lines) - 1);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?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\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\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
|
||||
final class ListBlockParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private ListBlock $block;
|
||||
|
||||
public function __construct(ListData $listData)
|
||||
{
|
||||
$this->block = new ListBlock($listData);
|
||||
}
|
||||
|
||||
public function getBlock(): ListBlock
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function isContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
return $childBlock instanceof ListItem;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
<?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\Extension\CommonMark\Parser\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListData;
|
||||
use League\CommonMark\Parser\Block\BlockStart;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\MarkdownParserStateInterface;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
final class ListBlockStartParser implements BlockStartParserInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ?ConfigurationInterface $config = null;
|
||||
|
||||
/**
|
||||
* @psalm-var non-empty-string|null
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private ?string $listMarkerRegex = null;
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->config = $configuration;
|
||||
}
|
||||
|
||||
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
|
||||
{
|
||||
if ($cursor->isIndented()) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$listData = $this->parseList($cursor, $parserState->getParagraphContent() !== null);
|
||||
if ($listData === null) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$listItemParser = new ListItemParser($listData);
|
||||
|
||||
// prepend the list block if needed
|
||||
$matched = $parserState->getLastMatchedBlockParser();
|
||||
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);
|
||||
}
|
||||
|
||||
return BlockStart::of($listItemParser)->at($cursor);
|
||||
}
|
||||
|
||||
private function parseList(Cursor $cursor, bool $inParagraph): ?ListData
|
||||
{
|
||||
$indent = $cursor->getIndent();
|
||||
|
||||
$tmpCursor = clone $cursor;
|
||||
$tmpCursor->advanceToNextNonSpaceOrTab();
|
||||
$rest = $tmpCursor->getRemainder();
|
||||
|
||||
if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) {
|
||||
$data = new ListData();
|
||||
$data->markerOffset = $indent;
|
||||
$data->type = ListBlock::TYPE_BULLET;
|
||||
$data->delimiter = null;
|
||||
$data->bulletChar = $rest[0];
|
||||
$markerLength = 1;
|
||||
} elseif (($matches = RegexHelper::matchFirst('/^(\d{1,9})([.)])/', $rest)) && (! $inParagraph || $matches[1] === '1')) {
|
||||
$data = new ListData();
|
||||
$data->markerOffset = $indent;
|
||||
$data->type = ListBlock::TYPE_ORDERED;
|
||||
$data->start = (int) $matches[1];
|
||||
$data->delimiter = $matches[2] === '.' ? ListBlock::DELIM_PERIOD : ListBlock::DELIM_PAREN;
|
||||
$data->bulletChar = null;
|
||||
$markerLength = \strlen($matches[0]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make sure we have spaces after
|
||||
$nextChar = $tmpCursor->peek($markerLength);
|
||||
if (! ($nextChar === null || $nextChar === "\t" || $nextChar === ' ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it interrupts paragraph, make sure first line isn't blank
|
||||
if ($inParagraph && ! RegexHelper::matchAt(RegexHelper::REGEX_NON_SPACE, $rest, $markerLength)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cursor->advanceToNextNonSpaceOrTab(); // to start of marker
|
||||
$cursor->advanceBy($markerLength, true); // to end of marker
|
||||
$data->padding = self::calculateListMarkerPadding($cursor, $markerLength);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private static function calculateListMarkerPadding(Cursor $cursor, int $markerLength): int
|
||||
{
|
||||
$start = $cursor->saveState();
|
||||
$spacesStartCol = $cursor->getColumn();
|
||||
|
||||
while ($cursor->getColumn() - $spacesStartCol < 5) {
|
||||
if (! $cursor->advanceBySpaceOrTab()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$blankItem = $cursor->peek() === null;
|
||||
$spacesAfterMarker = $cursor->getColumn() - $spacesStartCol;
|
||||
|
||||
if ($spacesAfterMarker >= 5 || $spacesAfterMarker < 1 || $blankItem) {
|
||||
$cursor->restoreState($start);
|
||||
$cursor->advanceBySpaceOrTab();
|
||||
|
||||
return $markerLength + 1;
|
||||
}
|
||||
|
||||
return $markerLength + $spacesAfterMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return non-empty-string
|
||||
*/
|
||||
private function generateListMarkerRegex(): string
|
||||
{
|
||||
// No configuration given - use the defaults
|
||||
if ($this->config === null) {
|
||||
return $this->listMarkerRegex = '/^[*+-]/';
|
||||
}
|
||||
|
||||
$markers = $this->config->get('commonmark/unordered_list_markers');
|
||||
\assert(\is_array($markers));
|
||||
|
||||
return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/';
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?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\Extension\CommonMark\Parser\Block;
|
||||
|
||||
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\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
|
||||
final class ListItemParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private ListItem $block;
|
||||
|
||||
public function __construct(ListData $listData)
|
||||
{
|
||||
$this->block = new ListItem($listData);
|
||||
}
|
||||
|
||||
public function getBlock(): ListItem
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function isContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
return ! $childBlock instanceof ListItem;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if ($cursor->isBlank()) {
|
||||
if ($this->block->firstChild() === null) {
|
||||
// Blank line after empty list item
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
$contentIndent = $this->block->getListData()->markerOffset + $this->getBlock()->getListData()->padding;
|
||||
if ($cursor->getIndent() >= $contentIndent) {
|
||||
$cursor->advanceBy($contentIndent, true);
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
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('`+');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$ticks = $inlineContext->getFullMatch();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$cursor->advanceBy($inlineContext->getFullMatchLength());
|
||||
|
||||
$currentPosition = $cursor->getPosition();
|
||||
$previousState = $cursor->saveState();
|
||||
|
||||
if ($this->findMatchingTicks(\strlen($ticks), $cursor)) {
|
||||
$code = $cursor->getSubstring($currentPosition, $cursor->getPosition() - $currentPosition - \strlen($ticks));
|
||||
|
||||
$c = \preg_replace('/\n/m', ' ', $code) ?? '';
|
||||
|
||||
if (
|
||||
$c !== '' &&
|
||||
$c[0] === ' ' &&
|
||||
\substr($c, -1, 1) === ' ' &&
|
||||
\preg_match('/[^ ]/', $c)
|
||||
) {
|
||||
$c = \substr($c, 1, -1);
|
||||
}
|
||||
|
||||
$inlineContext->getContainer()->appendChild(new Code($c));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we got here, we didn't match a closing backtick sequence
|
||||
$cursor->restoreState($previousState);
|
||||
$inlineContext->getContainer()->appendChild(new Text($ticks));
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
docker/streamline-src/vendor/league/commonmark/src/Extension/CommonMark/Parser/Inline/BangParser.php
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class BangParser implements InlineParserInterface
|
||||
{
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::string('![');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$cursor->advanceBy(2);
|
||||
|
||||
$node = new Text('![', ['delim' => true]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack for this opener
|
||||
$inlineContext->getDelimiterStack()->addBracket($node, $cursor->getPosition(), true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
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;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
|
||||
use League\CommonMark\Extension\Mention\Mention;
|
||||
use League\CommonMark\Node\Inline\AdjacentTextMerger;
|
||||
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;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceMapInterface;
|
||||
use League\CommonMark\Util\LinkParserHelper;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
final class CloseBracketParser implements InlineParserInterface, EnvironmentAwareInterface
|
||||
{
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private EnvironmentInterface $environment;
|
||||
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::string(']');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
// Look through stack of delimiters for a [ or !
|
||||
$opener = $inlineContext->getDelimiterStack()->getLastBracket();
|
||||
if ($opener === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $opener->isImage() && ! $opener->isActive()) {
|
||||
// no matched opener; remove from stack
|
||||
$inlineContext->getDelimiterStack()->removeBracket();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$cursor = $inlineContext->getCursor();
|
||||
|
||||
$startPos = $cursor->getPosition();
|
||||
$previousState = $cursor->saveState();
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
|
||||
// Check to see if we have a link/image
|
||||
|
||||
// Inline link?
|
||||
if ($result = $this->tryParseInlineLinkAndTitle($cursor)) {
|
||||
$link = $result;
|
||||
} elseif ($link = $this->tryParseReference($cursor, $inlineContext->getReferenceMap(), $opener, $startPos)) {
|
||||
$reference = $link;
|
||||
$link = ['url' => $link->getDestination(), 'title' => $link->getTitle()];
|
||||
} else {
|
||||
// No match; remove this opener from stack
|
||||
$inlineContext->getDelimiterStack()->removeBracket();
|
||||
$cursor->restoreState($previousState);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$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.
|
||||
if ($label instanceof Mention) {
|
||||
$label->replaceWith($replacement = new Text($label->getPrefix() . $label->getIdentifier()));
|
||||
$inline->appendChild($replacement);
|
||||
} elseif ($label instanceof Link) {
|
||||
foreach ($label->children() as $child) {
|
||||
$label->insertBefore($child);
|
||||
}
|
||||
|
||||
$label->detach();
|
||||
} else {
|
||||
$inline->appendChild($label);
|
||||
}
|
||||
}
|
||||
|
||||
// Process delimiters such as emphasis inside link/image
|
||||
$delimiterStack = $inlineContext->getDelimiterStack();
|
||||
$stackBottom = $opener->getPosition();
|
||||
$delimiterStack->processDelimiters($stackBottom, $this->environment->getDelimiterProcessors());
|
||||
$delimiterStack->removeBracket();
|
||||
$delimiterStack->removeAll($stackBottom);
|
||||
|
||||
// Merge any adjacent Text nodes together
|
||||
AdjacentTextMerger::mergeChildNodes($inline);
|
||||
|
||||
// processEmphasis will remove this and later delimiters.
|
||||
// Now, for a link, we also remove earlier link openers (no links in links)
|
||||
if (! $opener->isImage()) {
|
||||
$inlineContext->getDelimiterStack()->deactivateLinkOpeners();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setEnvironment(EnvironmentInterface $environment): void
|
||||
{
|
||||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
private function tryParseInlineLinkAndTitle(Cursor $cursor): ?array
|
||||
{
|
||||
if ($cursor->getCurrentCharacter() !== '(') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$previousState = $cursor->saveState();
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
if (($dest = LinkParserHelper::parseLinkDestination($cursor)) === null) {
|
||||
$cursor->restoreState($previousState);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
$previousCharacter = $cursor->peek(-1);
|
||||
// We know from previous lines that we've advanced at least one space so far, so this next call should never be null
|
||||
\assert(\is_string($previousCharacter));
|
||||
|
||||
$title = '';
|
||||
// make sure there's a space before the title:
|
||||
if (\preg_match(RegexHelper::REGEX_WHITESPACE_CHAR, $previousCharacter)) {
|
||||
$title = LinkParserHelper::parseLinkTitle($cursor) ?? '';
|
||||
}
|
||||
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
|
||||
if ($cursor->getCurrentCharacter() !== ')') {
|
||||
$cursor->restoreState($previousState);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
|
||||
return ['url' => $dest, 'title' => $title];
|
||||
}
|
||||
|
||||
private function tryParseReference(Cursor $cursor, ReferenceMapInterface $referenceMap, Bracket $opener, int $startPos): ?ReferenceInterface
|
||||
{
|
||||
$savePos = $cursor->saveState();
|
||||
$beforeLabel = $cursor->getPosition();
|
||||
$n = LinkParserHelper::parseLinkLabel($cursor);
|
||||
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);
|
||||
|
||||
if ($n === 0) {
|
||||
// If shortcut reference link, rewind before spaces we skipped
|
||||
$cursor->restoreState($savePos);
|
||||
}
|
||||
|
||||
return $referenceMap->get($referenceLabel);
|
||||
}
|
||||
|
||||
private function createInline(string $url, string $title, bool $isImage, ?ReferenceInterface $reference = null): AbstractWebResource
|
||||
{
|
||||
if ($isImage) {
|
||||
$inline = new Image($url, null, $title);
|
||||
} else {
|
||||
$inline = new Link($url, null, $title);
|
||||
}
|
||||
|
||||
if ($reference) {
|
||||
$inline->data->set('reference', $reference);
|
||||
}
|
||||
|
||||
return $inline;
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Parser\Inline;
|
||||
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class OpenBracketParser implements InlineParserInterface
|
||||
{
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::string('[');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$inlineContext->getCursor()->advanceBy(1);
|
||||
$node = new Text('[', ['delim' => true]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack for this opener
|
||||
$inlineContext->getDelimiterStack()->addBracket($node, $inlineContext->getCursor()->getPosition(), false);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\CommonMark\Renderer\Block;
|
||||
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
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;
|
||||
use League\CommonMark\Util\HtmlElement;
|
||||
use League\CommonMark\Xml\XmlNodeRendererInterface;
|
||||
|
||||
final class ListItemRenderer implements NodeRendererInterface, XmlNodeRendererInterface
|
||||
{
|
||||
/**
|
||||
* @param ListItem $node
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-suppress MoreSpecificImplementedParamType
|
||||
*/
|
||||
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
|
||||
{
|
||||
ListItem::assertInstanceOf($node);
|
||||
|
||||
$contents = $childRenderer->renderNodes($node->children());
|
||||
|
||||
$inTightList = ($parent = $node->parent()) && $parent instanceof TightBlockInterface && $parent->isTight();
|
||||
|
||||
if ($this->needsBlockSeparator($node->firstChild(), $inTightList)) {
|
||||
$contents = "\n" . $contents;
|
||||
}
|
||||
|
||||
if ($this->needsBlockSeparator($node->lastChild(), $inTightList)) {
|
||||
$contents .= "\n";
|
||||
}
|
||||
|
||||
$attrs = $node->data->get('attributes');
|
||||
|
||||
return new HtmlElement('li', $attrs, $contents);
|
||||
}
|
||||
|
||||
public function getXmlTagName(Node $node): string
|
||||
{
|
||||
return 'item';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getXmlAttributes(Node $node): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function needsBlockSeparator(?Node $child, bool $inTightList): bool
|
||||
{
|
||||
if ($child instanceof Paragraph && $inTightList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $child instanceof AbstractBlock;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\SmartPunct;
|
||||
|
||||
use League\CommonMark\Delimiter\Delimiter;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
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(Quote::SINGLE_QUOTE, Quote::DOUBLE_QUOTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes any quote characters found and manually adds them to the delimiter stack
|
||||
*/
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$char = $inlineContext->getFullMatch();
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$index = $cursor->getPosition();
|
||||
|
||||
$charBefore = $cursor->peek(-1);
|
||||
if ($charBefore === null) {
|
||||
$charBefore = "\n";
|
||||
}
|
||||
|
||||
$cursor->advance();
|
||||
|
||||
$charAfter = $cursor->getCurrentCharacter();
|
||||
if ($charAfter === null) {
|
||||
$charAfter = "\n";
|
||||
}
|
||||
|
||||
[$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter);
|
||||
$canOpen = $leftFlanking && ! $rightFlanking;
|
||||
$canClose = $rightFlanking;
|
||||
|
||||
$node = new Quote($char, ['delim' => true]);
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
// Add entry to stack to this opener
|
||||
$inlineContext->getDelimiterStack()->push(new Delimiter($char, 1, $node, $canOpen, $canClose, $index));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool[]
|
||||
*/
|
||||
private function determineFlanking(string $charBefore, string $charAfter): array
|
||||
{
|
||||
$afterIsWhitespace = \preg_match('/\pZ|\s/u', $charAfter);
|
||||
$afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
|
||||
$beforeIsWhitespace = \preg_match('/\pZ|\s/u', $charBefore);
|
||||
$beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
|
||||
|
||||
$leftFlanking = ! $afterIsWhitespace &&
|
||||
! ($afterIsPunctuation &&
|
||||
! $beforeIsWhitespace &&
|
||||
! $beforeIsPunctuation);
|
||||
|
||||
$rightFlanking = ! $beforeIsWhitespace &&
|
||||
! ($beforeIsPunctuation &&
|
||||
! $afterIsWhitespace &&
|
||||
! $afterIsPunctuation);
|
||||
|
||||
return [$leftFlanking, $rightFlanking];
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com> and uAfrica.com (http://uafrica.com)
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Extension\Strikethrough;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterInterface;
|
||||
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
|
||||
use League\CommonMark\Node\Inline\AbstractStringContainer;
|
||||
|
||||
final class StrikethroughDelimiterProcessor implements CacheableDelimiterProcessorInterface
|
||||
{
|
||||
public function getOpeningCharacter(): string
|
||||
{
|
||||
return '~';
|
||||
}
|
||||
|
||||
public function getClosingCharacter(): string
|
||||
{
|
||||
return '~';
|
||||
}
|
||||
|
||||
public function getMinLength(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
|
||||
{
|
||||
if ($opener->getLength() > 2 && $closer->getLength() > 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($opener->getLength() !== $closer->getLength()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// $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
|
||||
{
|
||||
$strikethrough = new Strikethrough(\str_repeat('~', $delimiterUse));
|
||||
|
||||
$tmp = $opener->next();
|
||||
while ($tmp !== null && $tmp !== $closer) {
|
||||
$next = $tmp->next();
|
||||
$strikethrough->appendChild($tmp);
|
||||
$tmp = $next;
|
||||
}
|
||||
|
||||
$opener->insertAfter($strikethrough);
|
||||
}
|
||||
|
||||
public function getCacheKey(DelimiterInterface $closer): string
|
||||
{
|
||||
return '~' . $closer->getLength();
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Martin Hasoň <martin.hason@gmail.com>
|
||||
* (c) Webuni s.r.o. <info@webuni.cz>
|
||||
* (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\Extension\Table;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\CommonMark\Renderer\HtmlDecorator;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class TableExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$attributeArraySchema = Expect::arrayOf(
|
||||
Expect::type('string|string[]|bool'), // attribute value(s)
|
||||
'string' // attribute name
|
||||
)->mergeDefaults(false);
|
||||
|
||||
$builder->addSchema('table', Expect::structure([
|
||||
'wrap' => Expect::structure([
|
||||
'enabled' => Expect::bool()->default(false),
|
||||
'tag' => Expect::string()->default('div'),
|
||||
'attributes' => Expect::arrayOf(Expect::string()),
|
||||
]),
|
||||
'alignment_attributes' => Expect::structure([
|
||||
'left' => (clone $attributeArraySchema)->default(['align' => 'left']),
|
||||
'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),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$tableRenderer = new TableRenderer();
|
||||
if ($environment->getConfiguration()->get('table/wrap/enabled')) {
|
||||
$tableRenderer = new HtmlDecorator($tableRenderer, $environment->getConfiguration()->get('table/wrap/tag'), $environment->getConfiguration()->get('table/wrap/attributes'));
|
||||
}
|
||||
|
||||
$environment
|
||||
->addBlockStartParser(new TableStartParser($environment->getConfiguration()->get('table/max_autocompleted_cells')))
|
||||
|
||||
->addRenderer(Table::class, $tableRenderer)
|
||||
->addRenderer(TableSection::class, new TableSectionRenderer())
|
||||
->addRenderer(TableRow::class, new TableRowRenderer())
|
||||
->addRenderer(TableCell::class, new TableCellRenderer($environment->getConfiguration()->get('table/alignment_attributes')));
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Martin Hasoň <martin.hason@gmail.com>
|
||||
* (c) Webuni s.r.o. <info@webuni.cz>
|
||||
* (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\Extension\Table;
|
||||
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\InlineParserEngineInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<string>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private ArrayCollection $bodyLines;
|
||||
|
||||
/**
|
||||
* @var array<int, string|null>
|
||||
* @psalm-var array<int, TableCell::ALIGN_*|null>
|
||||
* @phpstan-var array<int, TableCell::ALIGN_*|null>
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private array $columns;
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $headerCells;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private bool $nextIsSeparatorLine = true;
|
||||
|
||||
private int $remainingAutocompletedCells;
|
||||
|
||||
/**
|
||||
* @param array<int, string|null> $columns
|
||||
* @param array<int, string> $headerCells
|
||||
*
|
||||
* @psalm-param array<int, TableCell::ALIGN_*|null> $columns
|
||||
*
|
||||
* @phpstan-param array<int, TableCell::ALIGN_*|null> $columns
|
||||
*/
|
||||
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->remainingAutocompletedCells = $remainingAutocompletedCells;
|
||||
}
|
||||
|
||||
public function canHaveLazyContinuationLines(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getBlock(): Table
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if (\strpos($cursor->getLine(), '|') === false) {
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function addLine(string $line): void
|
||||
{
|
||||
if ($this->nextIsSeparatorLine) {
|
||||
$this->nextIsSeparatorLine = false;
|
||||
} else {
|
||||
$this->bodyLines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
public function parseInlines(InlineParserEngineInterface $inlineParser): void
|
||||
{
|
||||
$headerColumns = \count($this->headerCells);
|
||||
|
||||
$head = new TableSection(TableSection::TYPE_HEAD);
|
||||
$this->block->appendChild($head);
|
||||
|
||||
$headerRow = new TableRow();
|
||||
$head->appendChild($headerRow);
|
||||
for ($i = 0; $i < $headerColumns; $i++) {
|
||||
$cell = $this->headerCells[$i];
|
||||
$tableCell = $this->parseCell($cell, $i, $inlineParser);
|
||||
$tableCell->setType(TableCell::TYPE_HEADER);
|
||||
$headerRow->appendChild($tableCell);
|
||||
}
|
||||
|
||||
$body = null;
|
||||
foreach ($this->bodyLines as $rowLine) {
|
||||
$cells = self::split($rowLine);
|
||||
$row = new TableRow();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if ($body === null) {
|
||||
// It's valid to have a table without body. In that case, don't add an empty TableBody node.
|
||||
$body = new TableSection();
|
||||
$this->block->appendChild($body);
|
||||
}
|
||||
|
||||
$body->appendChild($row);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseCell(string $cell, int $column, InlineParserEngineInterface $inlineParser): TableCell
|
||||
{
|
||||
$tableCell = new TableCell(TableCell::TYPE_DATA, $this->columns[$column] ?? null);
|
||||
|
||||
if ($cell !== '') {
|
||||
$inlineParser->parse(\trim($cell), $tableCell);
|
||||
}
|
||||
|
||||
return $tableCell;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function split(string $line): array
|
||||
{
|
||||
$cursor = new Cursor(\trim($line));
|
||||
|
||||
if ($cursor->getCurrentCharacter() === '|') {
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
$cells = [];
|
||||
$sb = '';
|
||||
|
||||
while (! $cursor->isAtEnd()) {
|
||||
switch ($c = $cursor->getCurrentCharacter()) {
|
||||
case '\\':
|
||||
if ($cursor->peek() === '|') {
|
||||
// Pipe is special for table parsing. An escaped pipe doesn't result in a new cell, but is
|
||||
// passed down to inline parsing as an unescaped pipe. Note that that applies even for the `\|`
|
||||
// in an input like `\\|` - in other words, table parsing doesn't support escaping backslashes.
|
||||
$sb .= '|';
|
||||
$cursor->advanceBy(1);
|
||||
} else {
|
||||
// Preserve backslash before other characters or at end of line.
|
||||
$sb .= '\\';
|
||||
}
|
||||
|
||||
break;
|
||||
case '|':
|
||||
$cells[] = $sb;
|
||||
$sb = '';
|
||||
break;
|
||||
default:
|
||||
$sb .= $c;
|
||||
}
|
||||
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
if ($sb !== '') {
|
||||
$cells[] = $sb;
|
||||
}
|
||||
|
||||
return $cells;
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Martin Hasoň <martin.hason@gmail.com>
|
||||
* (c) Webuni s.r.o. <info@webuni.cz>
|
||||
* (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\Extension\Table;
|
||||
|
||||
use League\CommonMark\Parser\Block\BlockStart;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Block\ParagraphParser;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
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();
|
||||
if ($paragraph === null || \strpos($paragraph, '|') === false) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$columns = self::parseSeparator($cursor);
|
||||
if (\count($columns) === 0) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$lastLineBreak = \strrpos($paragraph, "\n");
|
||||
$lastLine = $lastLineBreak === false ? $paragraph : \substr($paragraph, $lastLineBreak + 1);
|
||||
|
||||
$headerCells = TableParser::split($lastLine);
|
||||
if (\count($headerCells) > \count($columns)) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
$cursor->advanceToEnd();
|
||||
|
||||
$parsers = [];
|
||||
|
||||
if ($lastLineBreak !== false) {
|
||||
$p = new ParagraphParser();
|
||||
$p->addLine(\substr($paragraph, 0, $lastLineBreak));
|
||||
$parsers[] = $p;
|
||||
}
|
||||
|
||||
$parsers[] = new TableParser($columns, $headerCells, $this->maxAutocompletedCells);
|
||||
|
||||
return BlockStart::of(...$parsers)
|
||||
->at($cursor)
|
||||
->replaceActiveBlockParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string|null>
|
||||
*
|
||||
* @psalm-return array<int, TableCell::ALIGN_*|null>
|
||||
*
|
||||
* @phpstan-return array<int, TableCell::ALIGN_*|null>
|
||||
*/
|
||||
private static function parseSeparator(Cursor $cursor): array
|
||||
{
|
||||
$columns = [];
|
||||
$pipes = 0;
|
||||
$valid = false;
|
||||
|
||||
while (! $cursor->isAtEnd()) {
|
||||
switch ($c = $cursor->getCurrentCharacter()) {
|
||||
case '|':
|
||||
$cursor->advanceBy(1);
|
||||
$pipes++;
|
||||
if ($pipes > 1) {
|
||||
// More than one adjacent pipe not allowed
|
||||
return [];
|
||||
}
|
||||
|
||||
// Need at least one pipe, even for a one-column table
|
||||
$valid = true;
|
||||
break;
|
||||
case '-':
|
||||
case ':':
|
||||
if ($pipes === 0 && \count($columns) > 0) {
|
||||
// Need a pipe after the first column (first column doesn't need to start with one)
|
||||
return [];
|
||||
}
|
||||
|
||||
$left = false;
|
||||
$right = false;
|
||||
if ($c === ':') {
|
||||
$left = true;
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
if ($cursor->match('/^-+/') === null) {
|
||||
// Need at least one dash
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($cursor->getCurrentCharacter() === ':') {
|
||||
$right = true;
|
||||
$cursor->advanceBy(1);
|
||||
}
|
||||
|
||||
$columns[] = self::getAlignment($left, $right);
|
||||
// Next, need another pipe
|
||||
$pipes = 0;
|
||||
break;
|
||||
case ' ':
|
||||
case "\t":
|
||||
// White space is allowed between pipes and columns
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
break;
|
||||
default:
|
||||
// Any other character is invalid
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (! $valid) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return TableCell::ALIGN_*|null
|
||||
*
|
||||
* @phpstan-return TableCell::ALIGN_*|null
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
private static function getAlignment(bool $left, bool $right): ?string
|
||||
{
|
||||
if ($left && $right) {
|
||||
return TableCell::ALIGN_CENTER;
|
||||
}
|
||||
|
||||
if ($left) {
|
||||
return TableCell::ALIGN_LEFT;
|
||||
}
|
||||
|
||||
if ($right) {
|
||||
return TableCell::ALIGN_RIGHT;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Node\Block;
|
||||
|
||||
class Paragraph extends AbstractBlock
|
||||
{
|
||||
/** @internal */
|
||||
public bool $onlyContainsLinkReferenceDefinitions = false;
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?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\Normalizer;
|
||||
|
||||
use League\Config\ConfigurationAwareInterface;
|
||||
use League\Config\ConfigurationInterface;
|
||||
|
||||
/**
|
||||
* Creates URL-friendly strings based on the given string input
|
||||
*/
|
||||
final class SlugNormalizer implements TextNormalizerInterface, ConfigurationAwareInterface
|
||||
{
|
||||
/** @psalm-allow-private-mutation */
|
||||
private int $defaultMaxLength = 255;
|
||||
|
||||
public function setConfiguration(ConfigurationInterface $configuration): void
|
||||
{
|
||||
$this->defaultMaxLength = $configuration->get('slug_normalizer/max_length');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
public function normalize(string $text, array $context = []): string
|
||||
{
|
||||
// Add any requested prefix
|
||||
$slug = ($context['prefix'] ?? '') . $text;
|
||||
// Trim whitespace
|
||||
$slug = \trim($slug);
|
||||
// Convert to lowercase
|
||||
$slug = \mb_strtolower($slug, 'UTF-8');
|
||||
// Try replacing whitespace with a dash
|
||||
$slug = \preg_replace('/\s+/u', '-', $slug) ?? $slug;
|
||||
// Try removing characters other than letters, numbers, and marks.
|
||||
$slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug) ?? $slug;
|
||||
// Trim to requested length if given
|
||||
if ($length = $context['length'] ?? $this->defaultMaxLength) {
|
||||
$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;
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Normalizer;
|
||||
|
||||
/***
|
||||
* Normalize text input using the steps given by the CommonMark spec to normalize labels
|
||||
*
|
||||
* @see https://spec.commonmark.org/0.29/#matches
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class TextNormalizer implements TextNormalizerInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public function normalize(string $text, array $context = []): string
|
||||
{
|
||||
// Collapse internal whitespace to single space and remove
|
||||
// leading/trailing whitespace
|
||||
$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');
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<?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\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;
|
||||
|
||||
/**
|
||||
* Parser implementation which ensures everything is added to the root-level Document
|
||||
*/
|
||||
final class DocumentBlockParser extends AbstractBlockContinueParser
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private Document $document;
|
||||
|
||||
public function __construct(ReferenceMapInterface $referenceMap)
|
||||
{
|
||||
$this->document = new Document($referenceMap);
|
||||
}
|
||||
|
||||
public function getBlock(): Document
|
||||
{
|
||||
return $this->document;
|
||||
}
|
||||
|
||||
public function isContainer(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function canContain(AbstractBlock $childBlock): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?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\Parser\Block;
|
||||
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\InlineParserEngineInterface;
|
||||
use League\CommonMark\Reference\ReferenceInterface;
|
||||
use League\CommonMark\Reference\ReferenceParser;
|
||||
|
||||
final class ParagraphParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private Paragraph $block;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private ReferenceParser $referenceParser;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->block = new Paragraph();
|
||||
$this->referenceParser = new ReferenceParser();
|
||||
}
|
||||
|
||||
public function canHaveLazyContinuationLines(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getBlock(): Paragraph
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
if ($cursor->isBlank()) {
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
public function addLine(string $line): void
|
||||
{
|
||||
$this->referenceParser->parse($line);
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
$this->block->onlyContainsLinkReferenceDefinitions = $this->referenceParser->hasReferences() && $this->referenceParser->getParagraphContent() === '';
|
||||
}
|
||||
|
||||
public function parseInlines(InlineParserEngineInterface $inlineParser): void
|
||||
{
|
||||
$content = $this->getContentString();
|
||||
if ($content !== '') {
|
||||
$inlineParser->parse($content, $this->block);
|
||||
}
|
||||
}
|
||||
|
||||
public function getContentString(): string
|
||||
{
|
||||
return $this->referenceParser->getParagraphContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReferenceInterface[]
|
||||
*/
|
||||
public function getReferences(): iterable
|
||||
{
|
||||
return $this->referenceParser->getReferences();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
<?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\Parser;
|
||||
|
||||
use League\CommonMark\Exception\UnexpectedEncodingException;
|
||||
|
||||
class Cursor
|
||||
{
|
||||
public const INDENT_LEVEL = 4;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private string $line;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private int $length;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* It's possible for this to be 1 char past the end, meaning we've parsed all chars and have
|
||||
* reached the end. In this state, any character-returning method MUST return null.
|
||||
*/
|
||||
private int $currentPosition = 0;
|
||||
|
||||
private int $column = 0;
|
||||
|
||||
private int $indent = 0;
|
||||
|
||||
private int $previousPosition = 0;
|
||||
|
||||
private ?int $nextNonSpaceCache = null;
|
||||
|
||||
private bool $partiallyConsumedTab = false;
|
||||
|
||||
/**
|
||||
* @var int|false
|
||||
*
|
||||
* @psalm-readonly
|
||||
*/
|
||||
private $lastTabPosition;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private bool $isMultibyte;
|
||||
|
||||
/** @var array<int, string> */
|
||||
private array $charCache = [];
|
||||
|
||||
/**
|
||||
* @param string $line The line being parsed (ASCII or UTF-8)
|
||||
*/
|
||||
public function __construct(string $line)
|
||||
{
|
||||
if (! \mb_check_encoding($line, 'UTF-8')) {
|
||||
throw new UnexpectedEncodingException('Unexpected encoding - UTF-8 or ASCII was expected');
|
||||
}
|
||||
|
||||
$this->line = $line;
|
||||
$this->length = \mb_strlen($line, 'UTF-8') ?: 0;
|
||||
$this->isMultibyte = $this->length !== \strlen($line);
|
||||
$this->lastTabPosition = $this->isMultibyte ? \mb_strrpos($line, "\t", 0, 'UTF-8') : \strrpos($line, "\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the next character which is not a space (or tab)
|
||||
*/
|
||||
public function getNextNonSpacePosition(): int
|
||||
{
|
||||
if ($this->nextNonSpaceCache !== null) {
|
||||
return $this->nextNonSpaceCache;
|
||||
}
|
||||
|
||||
if ($this->currentPosition >= $this->length) {
|
||||
return $this->length;
|
||||
}
|
||||
|
||||
$cols = $this->column;
|
||||
|
||||
for ($i = $this->currentPosition; $i < $this->length; $i++) {
|
||||
// This if-else was copied out of getCharacter() for performance reasons
|
||||
if ($this->isMultibyte) {
|
||||
$c = $this->charCache[$i] ??= \mb_substr($this->line, $i, 1, 'UTF-8');
|
||||
} else {
|
||||
$c = $this->line[$i];
|
||||
}
|
||||
|
||||
if ($c === ' ') {
|
||||
$cols++;
|
||||
} elseif ($c === "\t") {
|
||||
$cols += 4 - ($cols % 4);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->indent = $cols - $this->column;
|
||||
|
||||
return $this->nextNonSpaceCache = $i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next character which isn't a space (or tab)
|
||||
*/
|
||||
public function getNextNonSpaceCharacter(): ?string
|
||||
{
|
||||
$index = $this->getNextNonSpacePosition();
|
||||
if ($index >= $this->length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isMultibyte) {
|
||||
return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8');
|
||||
}
|
||||
|
||||
return $this->line[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the current indent (number of spaces after current position)
|
||||
*/
|
||||
public function getIndent(): int
|
||||
{
|
||||
if ($this->nextNonSpaceCache === null) {
|
||||
$this->getNextNonSpacePosition();
|
||||
}
|
||||
|
||||
return $this->indent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cursor is indented to INDENT_LEVEL
|
||||
*/
|
||||
public function isIndented(): bool
|
||||
{
|
||||
if ($this->nextNonSpaceCache === null) {
|
||||
$this->getNextNonSpacePosition();
|
||||
}
|
||||
|
||||
return $this->indent >= self::INDENT_LEVEL;
|
||||
}
|
||||
|
||||
public function getCharacter(?int $index = null): ?string
|
||||
{
|
||||
if ($index === null) {
|
||||
$index = $this->currentPosition;
|
||||
}
|
||||
|
||||
// Index out-of-bounds, or we're at the end
|
||||
if ($index < 0 || $index >= $this->length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isMultibyte) {
|
||||
return $this->charCache[$index] ??= \mb_substr($this->line, $index, 1, 'UTF-8');
|
||||
}
|
||||
|
||||
return $this->line[$index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly-optimized version of getCurrent(null)
|
||||
*/
|
||||
public function getCurrentCharacter(): ?string
|
||||
{
|
||||
if ($this->currentPosition >= $this->length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isMultibyte) {
|
||||
return $this->charCache[$this->currentPosition] ??= \mb_substr($this->line, $this->currentPosition, 1, 'UTF-8');
|
||||
}
|
||||
|
||||
return $this->line[$this->currentPosition];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next character (or null, if none) without advancing forwards
|
||||
*/
|
||||
public function peek(int $offset = 1): ?string
|
||||
{
|
||||
return $this->getCharacter($this->currentPosition + $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the remainder is blank
|
||||
*/
|
||||
public function isBlank(): bool
|
||||
{
|
||||
return $this->nextNonSpaceCache === $this->length || $this->getNextNonSpacePosition() === $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the cursor forwards
|
||||
*/
|
||||
public function advance(): void
|
||||
{
|
||||
$this->advanceBy(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the cursor forwards
|
||||
*
|
||||
* @param int $characters Number of characters to advance by
|
||||
* @param bool $advanceByColumns Whether to advance by columns instead of spaces
|
||||
*/
|
||||
public function advanceBy(int $characters, bool $advanceByColumns = false): void
|
||||
{
|
||||
$this->previousPosition = $this->currentPosition;
|
||||
$this->nextNonSpaceCache = null;
|
||||
|
||||
if ($this->currentPosition >= $this->length || $characters === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimization to avoid tab handling logic if we have no tabs
|
||||
if ($this->lastTabPosition === false || $this->currentPosition > $this->lastTabPosition) {
|
||||
$length = \min($characters, $this->length - $this->currentPosition);
|
||||
$this->partiallyConsumedTab = false;
|
||||
$this->currentPosition += $length;
|
||||
$this->column += $length;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$nextFewChars = $this->isMultibyte ?
|
||||
\mb_substr($this->line, $this->currentPosition, $characters, 'UTF-8') :
|
||||
\substr($this->line, $this->currentPosition, $characters);
|
||||
|
||||
if ($characters === 1) {
|
||||
$asArray = [$nextFewChars];
|
||||
} elseif ($this->isMultibyte) {
|
||||
/** @var string[] $asArray */
|
||||
$asArray = \mb_str_split($nextFewChars, 1, 'UTF-8');
|
||||
} else {
|
||||
$asArray = \str_split($nextFewChars);
|
||||
}
|
||||
|
||||
foreach ($asArray as $c) {
|
||||
if ($c === "\t") {
|
||||
$charsToTab = 4 - ($this->column % 4);
|
||||
if ($advanceByColumns) {
|
||||
$this->partiallyConsumedTab = $charsToTab > $characters;
|
||||
$charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab;
|
||||
$this->column += $charsToAdvance;
|
||||
$this->currentPosition += $this->partiallyConsumedTab ? 0 : 1;
|
||||
$characters -= $charsToAdvance;
|
||||
} else {
|
||||
$this->partiallyConsumedTab = false;
|
||||
$this->column += $charsToTab;
|
||||
$this->currentPosition++;
|
||||
$characters--;
|
||||
}
|
||||
} else {
|
||||
$this->partiallyConsumedTab = false;
|
||||
$this->currentPosition++;
|
||||
$this->column++;
|
||||
$characters--;
|
||||
}
|
||||
|
||||
if ($characters <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the cursor by a single space or tab, if present
|
||||
*/
|
||||
public function advanceBySpaceOrTab(): bool
|
||||
{
|
||||
$character = $this->getCurrentCharacter();
|
||||
|
||||
if ($character === ' ' || $character === "\t") {
|
||||
$this->advanceBy(1, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse zero or more space/tab characters
|
||||
*
|
||||
* @return int Number of positions moved
|
||||
*/
|
||||
public function advanceToNextNonSpaceOrTab(): int
|
||||
{
|
||||
$newPosition = $this->nextNonSpaceCache ?? $this->getNextNonSpacePosition();
|
||||
if ($newPosition === $this->currentPosition) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->advanceBy($newPosition - $this->currentPosition);
|
||||
$this->partiallyConsumedTab = false;
|
||||
|
||||
// We've just advanced to where that non-space is,
|
||||
// so any subsequent calls to find the next one will
|
||||
// always return the current position.
|
||||
$this->nextNonSpaceCache = $this->currentPosition;
|
||||
$this->indent = 0;
|
||||
|
||||
return $this->currentPosition - $this->previousPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse zero or more space characters, including at most one newline.
|
||||
*
|
||||
* Tab characters are not parsed with this function.
|
||||
*
|
||||
* @return int Number of positions moved
|
||||
*/
|
||||
public function advanceToNextNonSpaceOrNewline(): int
|
||||
{
|
||||
$currentCharacter = $this->getCurrentCharacter();
|
||||
|
||||
// Optimization: Avoid the regex if we know there are no spaces or newlines
|
||||
if ($currentCharacter !== ' ' && $currentCharacter !== "\n") {
|
||||
$this->previousPosition = $this->currentPosition;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$matches = [];
|
||||
\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);
|
||||
|
||||
return $this->currentPosition - $this->previousPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the position to the very end of the line
|
||||
*
|
||||
* @return int The number of characters moved
|
||||
*/
|
||||
public function advanceToEnd(): int
|
||||
{
|
||||
$this->previousPosition = $this->currentPosition;
|
||||
$this->nextNonSpaceCache = null;
|
||||
|
||||
$this->currentPosition = $this->length;
|
||||
|
||||
return $this->currentPosition - $this->previousPosition;
|
||||
}
|
||||
|
||||
public function getRemainder(): string
|
||||
{
|
||||
if ($this->currentPosition >= $this->length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$prefix = '';
|
||||
$position = $this->currentPosition;
|
||||
if ($this->partiallyConsumedTab) {
|
||||
$position++;
|
||||
$charsToTab = 4 - ($this->column % 4);
|
||||
$prefix = \str_repeat(' ', $charsToTab);
|
||||
}
|
||||
|
||||
$subString = $this->isMultibyte ?
|
||||
\mb_substr($this->line, $position, null, 'UTF-8') :
|
||||
\substr($this->line, $position);
|
||||
|
||||
return $prefix . $subString;
|
||||
}
|
||||
|
||||
public function getLine(): string
|
||||
{
|
||||
return $this->line;
|
||||
}
|
||||
|
||||
public function isAtEnd(): bool
|
||||
{
|
||||
return $this->currentPosition >= $this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to match a regular expression
|
||||
*
|
||||
* Returns the matching text and advances to the end of that match
|
||||
*
|
||||
* @psalm-param non-empty-string $regex
|
||||
*/
|
||||
public function match(string $regex): ?string
|
||||
{
|
||||
$subject = $this->getRemainder();
|
||||
|
||||
if (! \preg_match($regex, $subject, $matches, \PREG_OFFSET_CAPTURE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// $matches[0][0] contains the matched text
|
||||
// $matches[0][1] contains the index of that match
|
||||
|
||||
if ($this->isMultibyte) {
|
||||
// PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
|
||||
$offset = \mb_strlen(\substr($subject, 0, $matches[0][1]), 'UTF-8');
|
||||
$matchLength = \mb_strlen($matches[0][0], 'UTF-8');
|
||||
} else {
|
||||
$offset = $matches[0][1];
|
||||
$matchLength = \strlen($matches[0][0]);
|
||||
}
|
||||
|
||||
// [0][0] contains the matched text
|
||||
// [0][1] contains the index of that match
|
||||
$this->advanceBy($offset + $matchLength);
|
||||
|
||||
return $matches[0][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the current state of this cursor in case you need to rollback later.
|
||||
*
|
||||
* WARNING: Do not parse or use the return value for ANYTHING except for
|
||||
* passing it back into restoreState(), as the number of values and their
|
||||
* contents may change in any future release without warning.
|
||||
*/
|
||||
public function saveState(): CursorState
|
||||
{
|
||||
return new CursorState([
|
||||
$this->currentPosition,
|
||||
$this->previousPosition,
|
||||
$this->nextNonSpaceCache,
|
||||
$this->indent,
|
||||
$this->column,
|
||||
$this->partiallyConsumedTab,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the cursor to a previous state.
|
||||
*
|
||||
* Pass in the value previously obtained by calling saveState().
|
||||
*/
|
||||
public function restoreState(CursorState $state): void
|
||||
{
|
||||
[
|
||||
$this->currentPosition,
|
||||
$this->previousPosition,
|
||||
$this->nextNonSpaceCache,
|
||||
$this->indent,
|
||||
$this->column,
|
||||
$this->partiallyConsumedTab,
|
||||
] = $state->toArray();
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->currentPosition;
|
||||
}
|
||||
|
||||
public function getPreviousText(): string
|
||||
{
|
||||
if ($this->isMultibyte) {
|
||||
return \mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'UTF-8');
|
||||
}
|
||||
|
||||
return \substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition);
|
||||
}
|
||||
|
||||
public function getSubstring(int $start, ?int $length = null): string
|
||||
{
|
||||
if ($this->isMultibyte) {
|
||||
return \mb_substr($this->line, $start, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
if ($length !== null) {
|
||||
return \substr($this->line, $start, $length);
|
||||
}
|
||||
|
||||
return \substr($this->line, $start);
|
||||
}
|
||||
|
||||
public function getColumn(): int
|
||||
{
|
||||
return $this->column;
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Parser;
|
||||
|
||||
use League\CommonMark\Delimiter\DelimiterStack;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Reference\ReferenceMapInterface;
|
||||
|
||||
final class InlineParserContext
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private AbstractBlock $container;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private ReferenceMapInterface $referenceMap;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private Cursor $cursor;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private DelimiterStack $delimiterStack;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @psalm-var non-empty-array<string>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $matches;
|
||||
|
||||
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($maxDelimitersPerLine);
|
||||
}
|
||||
|
||||
public function getContainer(): AbstractBlock
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
public function getReferenceMap(): ReferenceMapInterface
|
||||
{
|
||||
return $this->referenceMap;
|
||||
}
|
||||
|
||||
public function getCursor(): Cursor
|
||||
{
|
||||
return $this->cursor;
|
||||
}
|
||||
|
||||
public function getDelimiterStack(): DelimiterStack
|
||||
{
|
||||
return $this->delimiterStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The full text that matched the InlineParserMatch definition
|
||||
*/
|
||||
public function getFullMatch(): string
|
||||
{
|
||||
return $this->matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int The length of the full match (in characters, not bytes)
|
||||
*/
|
||||
public function getFullMatchLength(): int
|
||||
{
|
||||
return \mb_strlen($this->matches[0], 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] Similar to preg_match(), index 0 will contain the full match, and any other array elements will be captured sub-matches
|
||||
*
|
||||
* @psalm-return non-empty-array<string>
|
||||
*/
|
||||
public function getMatches(): array
|
||||
{
|
||||
return $this->matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSubMatches(): array
|
||||
{
|
||||
return \array_slice($this->matches, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $matches
|
||||
*
|
||||
* @psalm-param non-empty-array<string> $matches
|
||||
*/
|
||||
public function withMatches(array $matches): InlineParserContext
|
||||
{
|
||||
$ctx = clone $this;
|
||||
|
||||
$ctx->matches = $matches;
|
||||
|
||||
return $ctx;
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Parser;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Node\Inline\AdjacentTextMerger;
|
||||
use League\CommonMark\Node\Inline\Text;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Reference\ReferenceMapInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class InlineParserEngine implements InlineParserEngineInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private EnvironmentInterface $environment;
|
||||
|
||||
/** @psalm-readonly */
|
||||
private ReferenceMapInterface $referenceMap;
|
||||
|
||||
/**
|
||||
* @var array<int, InlineParserInterface|string|bool>
|
||||
* @psalm-var list<array{0: InlineParserInterface, 1: non-empty-string, 2: bool}>
|
||||
* @phpstan-var array<int, array{0: InlineParserInterface, 1: non-empty-string, 2: bool}>
|
||||
*/
|
||||
private array $parsers = [];
|
||||
|
||||
public function __construct(EnvironmentInterface $environment, ReferenceMapInterface $referenceMap)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
$this->referenceMap = $referenceMap;
|
||||
|
||||
foreach ($environment->getInlineParsers() as $parser) {
|
||||
\assert($parser instanceof InlineParserInterface);
|
||||
$regex = $parser->getMatchDefinition()->getRegex();
|
||||
|
||||
$this->parsers[] = [$parser, $regex, \strlen($regex) !== \mb_strlen($regex, 'UTF-8')];
|
||||
}
|
||||
}
|
||||
|
||||
public function parse(string $contents, AbstractBlock $block): void
|
||||
{
|
||||
$contents = \trim($contents);
|
||||
$cursor = new Cursor($contents);
|
||||
|
||||
$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) {
|
||||
$currentPosition = $cursor->getPosition();
|
||||
// We've already gone past this point
|
||||
if ($currentPosition > $matchPosition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We've skipped over some uninteresting text that should be added as a plain text node
|
||||
if ($currentPosition < $matchPosition) {
|
||||
$cursor->advanceBy($matchPosition - $currentPosition);
|
||||
$this->addPlainText($cursor->getPreviousText(), $block);
|
||||
}
|
||||
|
||||
// We're now at a potential start - see which of the current parsers can handle it
|
||||
$parsed = false;
|
||||
foreach ($parsers as [$parser, $matches]) {
|
||||
\assert($parser instanceof InlineParserInterface);
|
||||
if ($parser->parse($inlineParserContext->withMatches($matches))) {
|
||||
// A parser has successfully handled the text at the given position; don't consider any others at this position
|
||||
$parsed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Despite potentially being interested, nothing actually parsed text here, so add the current character and continue onwards
|
||||
$this->addPlainText((string) $cursor->getCurrentCharacter(), $block);
|
||||
$cursor->advance();
|
||||
}
|
||||
|
||||
// Add any remaining text that wasn't parsed
|
||||
if (! $cursor->isAtEnd()) {
|
||||
$this->addPlainText($cursor->getRemainder(), $block);
|
||||
}
|
||||
|
||||
// Process any delimiters that were found
|
||||
$delimiterStack = $inlineParserContext->getDelimiterStack();
|
||||
$delimiterStack->processDelimiters(null, $this->environment->getDelimiterProcessors());
|
||||
$delimiterStack->removeAll();
|
||||
|
||||
// Combine adjacent text notes into one
|
||||
AdjacentTextMerger::mergeChildNodes($block);
|
||||
}
|
||||
|
||||
private function addPlainText(string $text, AbstractBlock $container): void
|
||||
{
|
||||
$lastInline = $container->lastChild();
|
||||
if ($lastInline instanceof Text && ! $lastInline->data->has('delim')) {
|
||||
$lastInline->append($text);
|
||||
} else {
|
||||
$container->appendChild(new Text($text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current line, ask all the parsers which parts of the text they would be interested in parsing.
|
||||
*
|
||||
* The resulting array provides a list of character positions, which parsers are interested in trying to parse
|
||||
* the text at those points, and (for convenience/optimization) what the matching text happened to be.
|
||||
*
|
||||
* @return array<array<int, InlineParserInterface|string>>
|
||||
*
|
||||
* @psalm-return array<int, list<array{0: InlineParserInterface, 1: non-empty-array<string>}>>
|
||||
*
|
||||
* @phpstan-return array<int, array<int, array{0: InlineParserInterface, 1: non-empty-array<string>}>>
|
||||
*/
|
||||
private function matchParsers(string $contents): array
|
||||
{
|
||||
$contents = \trim($contents);
|
||||
$isMultibyte = ! \mb_check_encoding($contents, 'ASCII');
|
||||
|
||||
$ret = [];
|
||||
|
||||
foreach ($this->parsers as [$parser, $regex, $isRegexMultibyte]) {
|
||||
if ($isMultibyte || $isRegexMultibyte) {
|
||||
$regex .= 'u';
|
||||
}
|
||||
|
||||
// See if the parser's InlineParserMatch regex matched against any part of the string
|
||||
if (! \preg_match_all($regex, $contents, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For each part that matched...
|
||||
foreach ($matches as $match) {
|
||||
if ($isMultibyte) {
|
||||
// PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
|
||||
$offset = \mb_strlen(\substr($contents, 0, $match[0][1]), 'UTF-8');
|
||||
} else {
|
||||
$offset = \intval($match[0][1]);
|
||||
}
|
||||
|
||||
// Remove the offsets, keeping only the matched text
|
||||
$m = \array_column($match, 0);
|
||||
|
||||
if ($m === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add this match to the list of character positions to stop at
|
||||
$ret[$offset][] = [$parser, $m];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by position so we visit them in order
|
||||
\ksort($ret);
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* Additional code based on commonmark-java (https://github.com/commonmark/commonmark-java)
|
||||
* - (c) Atlassian Pty Ltd
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Parser;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Event\DocumentPreParsedEvent;
|
||||
use League\CommonMark\Exception\CommonMarkException;
|
||||
use League\CommonMark\Input\MarkdownInput;
|
||||
use League\CommonMark\Node\Block\Document;
|
||||
use League\CommonMark\Node\Block\Paragraph;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserWithInlinesInterface;
|
||||
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;
|
||||
|
||||
final class MarkdownParser implements MarkdownParserInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private EnvironmentInterface $environment;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private int $maxNestingLevel;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private ReferenceMap $referenceMap;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private int $lineNumber = 0;
|
||||
|
||||
/** @psalm-readonly-allow-private-mutation */
|
||||
private Cursor $cursor;
|
||||
|
||||
/**
|
||||
* @var array<int, BlockContinueParserInterface>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $activeBlockParsers = [];
|
||||
|
||||
/**
|
||||
* @var array<int, BlockContinueParserWithInlinesInterface>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $closedBlockParsers = [];
|
||||
|
||||
public function __construct(EnvironmentInterface $environment)
|
||||
{
|
||||
$this->environment = $environment;
|
||||
}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
$this->referenceMap = new ReferenceMap();
|
||||
$this->lineNumber = 0;
|
||||
$this->activeBlockParsers = [];
|
||||
$this->closedBlockParsers = [];
|
||||
|
||||
$this->maxNestingLevel = $this->environment->getConfiguration()->get('max_nesting_level');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws CommonMarkException
|
||||
*/
|
||||
public function parse(string $input): Document
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$documentParser = new DocumentBlockParser($this->referenceMap);
|
||||
$this->activateBlockParser($documentParser);
|
||||
|
||||
$preParsedEvent = new DocumentPreParsedEvent($documentParser->getBlock(), new MarkdownInput($input));
|
||||
$this->environment->dispatch($preParsedEvent);
|
||||
$markdownInput = $preParsedEvent->getMarkdown();
|
||||
|
||||
foreach ($markdownInput->getLines() as $lineNumber => $line) {
|
||||
$this->lineNumber = $lineNumber;
|
||||
$this->parseLine($line);
|
||||
}
|
||||
|
||||
// finalizeAndProcess
|
||||
$this->closeBlockParsers(\count($this->activeBlockParsers), $this->lineNumber);
|
||||
$this->processInlines(\strlen($input));
|
||||
|
||||
$this->environment->dispatch(new DocumentParsedEvent($documentParser->getBlock()));
|
||||
|
||||
return $documentParser->getBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each
|
||||
* line of input, then finalizing the document.
|
||||
*/
|
||||
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();
|
||||
if ($matches === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unmatchedBlocks = \count($this->activeBlockParsers) - $matches;
|
||||
$blockParser = $this->activeBlockParsers[$matches - 1];
|
||||
$startedNewBlock = false;
|
||||
|
||||
// Unless last matched container is a code block, try new container starts,
|
||||
// adding children to the last matched container:
|
||||
$tryBlockStarts = $blockParser->getBlock() instanceof Paragraph || $blockParser->isContainer();
|
||||
while ($tryBlockStarts) {
|
||||
// this is a little performance optimization
|
||||
if ($this->cursor->isBlank()) {
|
||||
$this->cursor->advanceToEnd();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($blockParser->getBlock()->getDepth() >= $this->maxNestingLevel) {
|
||||
break;
|
||||
}
|
||||
|
||||
$blockStart = $this->findBlockStart($blockParser);
|
||||
if ($blockStart === null || $blockStart->isAborting()) {
|
||||
$this->cursor->advanceToNextNonSpaceOrTab();
|
||||
break;
|
||||
}
|
||||
|
||||
if (($state = $blockStart->getCursorState()) !== null) {
|
||||
$this->cursor->restoreState($state);
|
||||
}
|
||||
|
||||
$startedNewBlock = true;
|
||||
|
||||
// We're starting a new block. If we have any previous blocks that need to be closed, we need to do it now.
|
||||
if ($unmatchedBlocks > 0) {
|
||||
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
|
||||
$unmatchedBlocks = 0;
|
||||
}
|
||||
|
||||
$oldBlockLineStart = null;
|
||||
if ($blockStart->isReplaceActiveBlockParser()) {
|
||||
$oldBlockLineStart = $this->prepareActiveBlockParserForReplacement();
|
||||
}
|
||||
|
||||
foreach ($blockStart->getBlockParsers() as $newBlockParser) {
|
||||
$blockParser = $this->addChild($newBlockParser, $oldBlockLineStart);
|
||||
$tryBlockStarts = $newBlockParser->isContainer();
|
||||
}
|
||||
}
|
||||
|
||||
// What remains at the offset is a text line. Add the text to the appropriate block.
|
||||
|
||||
// First check for a lazy paragraph continuation:
|
||||
if (! $startedNewBlock && ! $this->cursor->isBlank() && $this->getActiveBlockParser()->canHaveLazyContinuationLines()) {
|
||||
$this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
|
||||
} else {
|
||||
// finalize any blocks not matched
|
||||
if ($unmatchedBlocks > 0) {
|
||||
$this->closeBlockParsers($unmatchedBlocks, $this->lineNumber - 1);
|
||||
}
|
||||
|
||||
if (! $blockParser->isContainer()) {
|
||||
$this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
|
||||
} elseif (! $this->cursor->isBlank()) {
|
||||
$this->addChild(new ParagraphParser());
|
||||
$this->getActiveBlockParser()->addLine($this->cursor->getRemainder());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parseBlockContinuation(): ?int
|
||||
{
|
||||
// For each containing block, try to parse the associated line start.
|
||||
// The document will always match, so we can skip the first block parser and start at 1 matches
|
||||
$matches = 1;
|
||||
for ($i = 1; $i < \count($this->activeBlockParsers); $i++) {
|
||||
$blockParser = $this->activeBlockParsers[$i];
|
||||
$blockContinue = $blockParser->tryContinue(clone $this->cursor, $this->getActiveBlockParser());
|
||||
if ($blockContinue === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($blockContinue->isFinalize()) {
|
||||
$this->closeBlockParsers(\count($this->activeBlockParsers) - $i, $this->lineNumber);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (($state = $blockContinue->getCursorState()) !== null) {
|
||||
$this->cursor->restoreState($state);
|
||||
}
|
||||
|
||||
$matches++;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
private function findBlockStart(BlockContinueParserInterface $lastMatchedBlockParser): ?BlockStart
|
||||
{
|
||||
$matchedBlockParser = new MarkdownParserState($this->getActiveBlockParser(), $lastMatchedBlockParser);
|
||||
|
||||
foreach ($this->environment->getBlockStartParsers() as $blockStartParser) {
|
||||
\assert($blockStartParser instanceof BlockStartParserInterface);
|
||||
if (($result = $blockStartParser->tryStart(clone $this->cursor, $matchedBlockParser)) !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function closeBlockParsers(int $count, int $endLineNumber): void
|
||||
{
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$blockParser = $this->deactivateBlockParser();
|
||||
$this->finalize($blockParser, $endLineNumber);
|
||||
|
||||
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
|
||||
if ($blockParser instanceof BlockContinueParserWithInlinesInterface) {
|
||||
// Remember for inline parsing
|
||||
$this->closedBlockParsers[] = $blockParser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize a block. Close it and do any necessary postprocessing, e.g. creating string_content from strings,
|
||||
* setting the 'tight' or 'loose' status of a list, and parsing the beginnings of paragraphs for reference
|
||||
* definitions.
|
||||
*/
|
||||
private function finalize(BlockContinueParserInterface $blockParser, int $endLineNumber): void
|
||||
{
|
||||
if ($blockParser instanceof ParagraphParser) {
|
||||
$this->updateReferenceMap($blockParser->getReferences());
|
||||
}
|
||||
|
||||
$blockParser->getBlock()->setEndLine($endLineNumber);
|
||||
$blockParser->closeBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through a block & children recursively, parsing string content into inline content where appropriate.
|
||||
*/
|
||||
private function processInlines(int $inputSize): void
|
||||
{
|
||||
$p = new InlineParserEngine($this->environment, new MemoryLimitedReferenceMap($this->referenceMap, $inputSize));
|
||||
|
||||
foreach ($this->closedBlockParsers as $blockParser) {
|
||||
$blockParser->parseInlines($p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, ?int $startLineNumber = null): BlockContinueParserInterface
|
||||
{
|
||||
$blockParser->getBlock()->setStartLine($startLineNumber ?? $this->lineNumber);
|
||||
|
||||
while (! $this->getActiveBlockParser()->canContain($blockParser->getBlock())) {
|
||||
$this->closeBlockParsers(1, ($startLineNumber ?? $this->lineNumber) - 1);
|
||||
}
|
||||
|
||||
$this->getActiveBlockParser()->getBlock()->appendChild($blockParser->getBlock());
|
||||
$this->activateBlockParser($blockParser);
|
||||
|
||||
return $blockParser;
|
||||
}
|
||||
|
||||
private function activateBlockParser(BlockContinueParserInterface $blockParser): void
|
||||
{
|
||||
$this->activeBlockParsers[] = $blockParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParserLogicException
|
||||
*/
|
||||
private function deactivateBlockParser(): BlockContinueParserInterface
|
||||
{
|
||||
$popped = \array_pop($this->activeBlockParsers);
|
||||
if ($popped === null) {
|
||||
throw new ParserLogicException('The last block parser should not be deactivated');
|
||||
}
|
||||
|
||||
return $popped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
|
||||
if ($old instanceof ParagraphParser) {
|
||||
$this->updateReferenceMap($old->getReferences());
|
||||
}
|
||||
|
||||
$old->getBlock()->detach();
|
||||
|
||||
return $old->getBlock()->getStartLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReferenceInterface[] $references
|
||||
*/
|
||||
private function updateReferenceMap(iterable $references): void
|
||||
{
|
||||
foreach ($references as $reference) {
|
||||
if (! $this->referenceMap->contains($reference->getLabel())) {
|
||||
$this->referenceMap->add($reference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParserLogicException
|
||||
*/
|
||||
public function getActiveBlockParser(): BlockContinueParserInterface
|
||||
{
|
||||
$active = \end($this->activeBlockParsers);
|
||||
if ($active === false) {
|
||||
throw new ParserLogicException('No active block parsers are available');
|
||||
}
|
||||
|
||||
return $active;
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Reference;
|
||||
|
||||
use League\CommonMark\Normalizer\TextNormalizer;
|
||||
|
||||
/**
|
||||
* A collection of references, indexed by label
|
||||
*/
|
||||
final class ReferenceMap implements ReferenceMapInterface
|
||||
{
|
||||
/** @psalm-readonly */
|
||||
private TextNormalizer $normalizer;
|
||||
|
||||
/**
|
||||
* @var array<string, ReferenceInterface>
|
||||
*
|
||||
* @psalm-readonly-allow-private-mutation
|
||||
*/
|
||||
private array $references = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->normalizer = new TextNormalizer();
|
||||
}
|
||||
|
||||
public function add(ReferenceInterface $reference): void
|
||||
{
|
||||
// Normalize the key
|
||||
$key = $this->normalizer->normalize($reference->getLabel());
|
||||
// Store the reference
|
||||
$this->references[$key] = $reference;
|
||||
}
|
||||
|
||||
public function contains(string $label): bool
|
||||
{
|
||||
if ($this->references === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$label = $this->normalizer->normalize($label);
|
||||
|
||||
return isset($this->references[$label]);
|
||||
}
|
||||
|
||||
public function get(string $label): ?ReferenceInterface
|
||||
{
|
||||
if ($this->references === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$label = $this->normalizer->normalize($label);
|
||||
|
||||
return $this->references[$label] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Traversable<string, ReferenceInterface>
|
||||
*/
|
||||
public function getIterator(): \Traversable
|
||||
{
|
||||
foreach ($this->references as $normalizedLabel => $reference) {
|
||||
yield $normalizedLabel => $reference;
|
||||
}
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->references);
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Util;
|
||||
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class LinkParserHelper
|
||||
{
|
||||
/**
|
||||
* Attempt to parse link destination
|
||||
*
|
||||
* @return string|null The string, or null if no match
|
||||
*/
|
||||
public static function parseLinkDestination(Cursor $cursor): ?string
|
||||
{
|
||||
if ($cursor->getCurrentCharacter() === '<') {
|
||||
return self::parseDestinationBraces($cursor);
|
||||
}
|
||||
|
||||
$destination = self::manuallyParseLinkDestination($cursor);
|
||||
if ($destination === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UrlEncoder::unescapeAndEncode(
|
||||
RegexHelper::unescape($destination)
|
||||
);
|
||||
}
|
||||
|
||||
public static function parseLinkLabel(Cursor $cursor): int
|
||||
{
|
||||
$match = $cursor->match('/^\[(?:[^\\\\\[\]]|\\\\.){0,1000}\]/');
|
||||
if ($match === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$length = \mb_strlen($match, 'UTF-8');
|
||||
|
||||
if ($length > 1001) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $length;
|
||||
}
|
||||
|
||||
public static function parsePartialLinkLabel(Cursor $cursor): ?string
|
||||
{
|
||||
return $cursor->match('/^(?:[^\\\\\[\]]++|\\\\.?)*+/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse link title (sans quotes)
|
||||
*
|
||||
* @return string|null The string, or null if no match
|
||||
*/
|
||||
public static function parseLinkTitle(Cursor $cursor): ?string
|
||||
{
|
||||
if ($title = $cursor->match('/' . RegexHelper::PARTIAL_LINK_TITLE . '/')) {
|
||||
// Chop off quotes from title and unescape
|
||||
return RegexHelper::unescape(\substr($title, 1, -1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function parsePartialLinkTitle(Cursor $cursor, string $endDelimiter): ?string
|
||||
{
|
||||
$endDelimiter = \preg_quote($endDelimiter, '/');
|
||||
$regex = \sprintf('/(%s|[^%s\x00])*(?:%s)?/', RegexHelper::PARTIAL_ESCAPED_CHAR, $endDelimiter, $endDelimiter);
|
||||
if (($partialTitle = $cursor->match($regex)) === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return RegexHelper::unescape($partialTitle);
|
||||
}
|
||||
|
||||
private static function manuallyParseLinkDestination(Cursor $cursor): ?string
|
||||
{
|
||||
$remainder = $cursor->getRemainder();
|
||||
$openParens = 0;
|
||||
$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 === '(') {
|
||||
$openParens++;
|
||||
// Limit to 32 nested parens for pathological cases
|
||||
if ($openParens > 32) {
|
||||
return null;
|
||||
}
|
||||
} elseif ($c === ')') {
|
||||
if ($openParens < 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
$openParens--;
|
||||
} elseif (\ord($c) <= 32 && RegexHelper::isWhitespace($c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($openParens !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($i === 0 && (! isset($c) || $c !== ')')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$destination = \substr($remainder, 0, $i);
|
||||
$cursor->advanceBy(\mb_strlen($destination, 'UTF-8'));
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
*
|
||||
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
|
||||
* - (c) John MacFarlane
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace League\CommonMark\Util;
|
||||
|
||||
use League\CommonMark\Exception\InvalidArgumentException;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock;
|
||||
|
||||
/**
|
||||
* Provides regular expressions and utilities for parsing Markdown
|
||||
*
|
||||
* All of the PARTIAL_ regex constants assume that they'll be used in case-insensitive searches
|
||||
* All other complete regexes provided by this class (either via constants or methods) will have case-insensitivity enabled.
|
||||
*
|
||||
* @phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found
|
||||
*
|
||||
* @psalm-immutable
|
||||
*/
|
||||
final class RegexHelper
|
||||
{
|
||||
// Partial regular expressions (wrap with `/` on each side and add the case-insensitive `i` flag before use)
|
||||
public const PARTIAL_ENTITY = '&(?:#x[a-f0-9]{1,6}|#[0-9]{1,7}|[a-z][a-z0-9]{1,31});';
|
||||
public const PARTIAL_ESCAPABLE = '[!"#$%&\'()*+,.\/:;<=>?@[\\\\\]^_`{|}~-]';
|
||||
public const PARTIAL_ESCAPED_CHAR = '\\\\' . self::PARTIAL_ESCAPABLE;
|
||||
public const PARTIAL_IN_DOUBLE_QUOTES = '"(' . self::PARTIAL_ESCAPED_CHAR . '|[^"\x00])*"';
|
||||
public const PARTIAL_IN_SINGLE_QUOTES = '\'(' . self::PARTIAL_ESCAPED_CHAR . '|[^\'\x00])*\'';
|
||||
public const PARTIAL_IN_PARENS = '\\((' . self::PARTIAL_ESCAPED_CHAR . '|[^)\x00])*\\)';
|
||||
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|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 = '\'[^\']*\'';
|
||||
public const PARTIAL_DOUBLEQUOTEDVALUE = '"[^"]*"';
|
||||
public const PARTIAL_ATTRIBUTEVALUE = '(?:' . self::PARTIAL_UNQUOTEDVALUE . '|' . self::PARTIAL_SINGLEQUOTEDVALUE . '|' . self::PARTIAL_DOUBLEQUOTEDVALUE . ')';
|
||||
public const PARTIAL_ATTRIBUTEVALUESPEC = '(?:' . '\s*=' . '\s*' . self::PARTIAL_ATTRIBUTEVALUE . ')';
|
||||
public const PARTIAL_ATTRIBUTE = '(?:' . '\s+' . self::PARTIAL_ATTRIBUTENAME . self::PARTIAL_ATTRIBUTEVALUESPEC . '?)';
|
||||
public const PARTIAL_OPENTAG = '<' . self::PARTIAL_TAGNAME . self::PARTIAL_ATTRIBUTE . '*' . '\s*\/?>';
|
||||
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 = '<!-->|<!--->|<!--[\s\S]*?-->';
|
||||
public const PARTIAL_PROCESSINGINSTRUCTION = '[<][?][\s\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 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]/';
|
||||
|
||||
public const REGEX_WHITESPACE_CHAR = '/^[ \t\n\x0b\x0c\x0d]/';
|
||||
public const REGEX_UNICODE_WHITESPACE_CHAR = '/^\pZ|\s/u';
|
||||
public const REGEX_THEMATIC_BREAK = '/^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$/';
|
||||
public const REGEX_LINK_DESTINATION_BRACES = '/^(?:<(?:[^<>\\n\\\\\\x00]|\\\\.)*>)/';
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function isEscapable(string $character): bool
|
||||
{
|
||||
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
|
||||
*/
|
||||
public static function isLetter(?string $character): bool
|
||||
{
|
||||
if ($character === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \preg_match('/[\pL]/u', $character) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to match a regex in string s at offset offset
|
||||
*
|
||||
* @psalm-param non-empty-string $regex
|
||||
*
|
||||
* @return int|null Index of match, or null
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function matchAt(string $regex, string $string, int $offset = 0): ?int
|
||||
{
|
||||
$matches = [];
|
||||
$string = \mb_substr($string, $offset, null, 'UTF-8');
|
||||
if (! \preg_match($regex, $string, $matches, \PREG_OFFSET_CAPTURE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
|
||||
$charPos = \mb_strlen(\mb_strcut($string, 0, $matches[0][1], 'UTF-8'), 'UTF-8');
|
||||
|
||||
return $offset + $charPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional wrapper around preg_match_all which only returns the first set of matches
|
||||
*
|
||||
* @psalm-param non-empty-string $pattern
|
||||
*
|
||||
* @return string[]|null
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function matchFirst(string $pattern, string $subject, int $offset = 0): ?array
|
||||
{
|
||||
if ($offset !== 0) {
|
||||
$subject = \substr($subject, $offset);
|
||||
}
|
||||
|
||||
\preg_match_all($pattern, $subject, $matches, \PREG_SET_ORDER);
|
||||
|
||||
if ($matches === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $matches[0] ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace backslash escapes with literal characters
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function unescape(string $string): string
|
||||
{
|
||||
$allEscapedChar = '/\\\\(' . self::PARTIAL_ESCAPABLE . ')/';
|
||||
|
||||
$escaped = \preg_replace($allEscapedChar, '$1', $string);
|
||||
\assert(\is_string($escaped));
|
||||
|
||||
return \preg_replace_callback('/' . self::PARTIAL_ENTITY . '/i', static fn ($e) => Html5EntityDecoder::decode($e[0]), $escaped);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param int $type HTML block type
|
||||
*
|
||||
* @psalm-param HtmlBlock::TYPE_* $type
|
||||
*
|
||||
* @phpstan-param HtmlBlock::TYPE_* $type
|
||||
*
|
||||
* @psalm-return non-empty-string
|
||||
*
|
||||
* @throws InvalidArgumentException if an invalid type is given
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getHtmlBlockOpenRegex(int $type): string
|
||||
{
|
||||
switch ($type) {
|
||||
case HtmlBlock::TYPE_1_CODE_CONTAINER:
|
||||
return '/^<(?:script|pre|textarea|style)(?:\s|>|$)/i';
|
||||
case HtmlBlock::TYPE_2_COMMENT:
|
||||
return '/^<!--/';
|
||||
case HtmlBlock::TYPE_3:
|
||||
return '/^<[?]/';
|
||||
case HtmlBlock::TYPE_4:
|
||||
return '/^<![A-Z]/i';
|
||||
case HtmlBlock::TYPE_5_CDATA:
|
||||
return '/^<!\[CDATA\[/i';
|
||||
case HtmlBlock::TYPE_6_BLOCK_ELEMENT:
|
||||
return '%^<[/]?(?: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|h[123456]|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)(?:\s|[/]?[>]|$)%i';
|
||||
case HtmlBlock::TYPE_7_MISC_ELEMENT:
|
||||
return '/^(?:' . self::PARTIAL_OPENTAG . '|' . self::PARTIAL_CLOSETAG . ')\\s*$/i';
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid HTML block type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @param int $type HTML block type
|
||||
*
|
||||
* @psalm-param HtmlBlock::TYPE_* $type
|
||||
*
|
||||
* @phpstan-param HtmlBlock::TYPE_* $type
|
||||
*
|
||||
* @psalm-return non-empty-string
|
||||
*
|
||||
* @throws InvalidArgumentException if an invalid type is given
|
||||
*
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getHtmlBlockCloseRegex(int $type): string
|
||||
{
|
||||
switch ($type) {
|
||||
case HtmlBlock::TYPE_1_CODE_CONTAINER:
|
||||
return '%<\/(?:script|pre|textarea|style)>%i';
|
||||
case HtmlBlock::TYPE_2_COMMENT:
|
||||
return '/-->/';
|
||||
case HtmlBlock::TYPE_3:
|
||||
return '/\?>/';
|
||||
case HtmlBlock::TYPE_4:
|
||||
return '/>/';
|
||||
case HtmlBlock::TYPE_5_CDATA:
|
||||
return '/\]\]>/';
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid HTML block type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function isLinkPotentiallyUnsafe(string $url): bool
|
||||
{
|
||||
return \preg_match(self::REGEX_UNSAFE_PROTOCOL, $url) !== 0 && \preg_match(self::REGEX_SAFE_DATA_PROTOCOL, $url) === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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\Util;
|
||||
|
||||
use League\CommonMark\Exception\IOException;
|
||||
|
||||
/**
|
||||
* Reads in a CommonMark spec document and extracts the input/output examples for testing against them
|
||||
*/
|
||||
final class SpecReader
|
||||
{
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}>
|
||||
*/
|
||||
public static function read(string $data): iterable
|
||||
{
|
||||
// Normalize newlines for platform independence
|
||||
$data = \preg_replace('/\r\n?/', "\n", $data);
|
||||
\assert($data !== null);
|
||||
$data = \preg_replace('/<!-- END TESTS -->.*$/', '', $data);
|
||||
\assert($data !== null);
|
||||
\preg_match_all('/^`{32} (example ?\w*)\n([\s\S]*?)^\.\n([\s\S]*?)^`{32}$|^#{1,6} *(.*)$/m', $data, $matches, PREG_SET_ORDER);
|
||||
|
||||
$currentSection = 'Example';
|
||||
$exampleNumber = 0;
|
||||
|
||||
foreach ($matches as $match) {
|
||||
\assert(isset($match[1], $match[2], $match[3]));
|
||||
if (isset($match[4])) {
|
||||
$currentSection = $match[4];
|
||||
continue;
|
||||
}
|
||||
|
||||
yield \trim($currentSection . ' #' . $exampleNumber) => [
|
||||
'input' => \str_replace('→', "\t", $match[2]),
|
||||
'output' => \str_replace('→', "\t", $match[3]),
|
||||
'type' => $match[1],
|
||||
'section' => $currentSection,
|
||||
'number' => $exampleNumber++,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{input: string, output: string, type: string, section: string, number: int}>
|
||||
*
|
||||
* @throws IOException if the file cannot be loaded
|
||||
*/
|
||||
public static function readFile(string $filename): iterable
|
||||
{
|
||||
if (($data = \file_get_contents($filename)) === false) {
|
||||
throw new IOException(\sprintf('Failed to load spec from %s', $filename));
|
||||
}
|
||||
|
||||
return self::read($data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user