This commit is contained in:
GZod01 2025-02-07 15:48:54 +01:00
parent 3116d650da
commit 412652b9dc
453 changed files with 39152 additions and 1629 deletions

View file

@ -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'));
}

727
vendor/league/commonmark/CHANGELOG.md vendored Normal file
View file

@ -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

28
vendor/league/commonmark/LICENSE vendored Normal file
View file

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2014-2022, Colin O'Dell. All rights reserved. Some code based on commonmark.js (copyright 2014-2018, John MacFarlane) and commonmark-java (copyright 2015-2016, Atlassian Pty Ltd)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

223
vendor/league/commonmark/README.md vendored Normal file
View file

@ -0,0 +1,223 @@
# league/commonmark
[![Latest Version](https://img.shields.io/packagist/v/league/commonmark.svg?style=flat-square)](https://packagist.org/packages/league/commonmark)
[![Total Downloads](https://img.shields.io/packagist/dt/league/commonmark.svg?style=flat-square)](https://packagist.org/packages/league/commonmark)
[![Software License](https://img.shields.io/badge/License-BSD--3-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/thephpleague/commonmark/tests.yml?branch=main&style=flat-square)](https://github.com/thephpleague/commonmark/actions?query=workflow%3ATests+branch%3Amain)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/commonmark.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/commonmark/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/commonmark.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/commonmark)
[![Psalm Type Coverage](https://shepherd.dev/github/thephpleague/commonmark/coverage.svg)](https://shepherd.dev/github/thephpleague/commonmark)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/126/badge)](https://bestpractices.coreinfrastructure.org/projects/126)
[![Sponsor development of this project](https://img.shields.io/badge/sponsor%20this%20package-%E2%9D%A4-ff69b4.svg?style=flat-square)](https://www.colinodell.com/sponsor)
![league/commonmark](commonmark-banner.png)
**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

128
vendor/league/commonmark/composer.json vendored Normal file
View file

@ -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
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
/**
* Converts CommonMark-compatible Markdown to HTML.
*/
final class CommonMarkConverter extends MarkdownConverter
{
/**
* Create a new Markdown converter pre-configured for CommonMark
*
* @param array<string, mixed> $config
*/
public function __construct(array $config = [])
{
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
parent::__construct($environment);
}
public function getEnvironment(): Environment
{
\assert($this->environment instanceof Environment);
return $this->environment;
}
}

View file

@ -0,0 +1,30 @@
<?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;
use League\CommonMark\Exception\CommonMarkException;
use League\CommonMark\Output\RenderedContentInterface;
use League\Config\Exception\ConfigurationExceptionInterface;
/**
* Interface for a service which converts content from one format (like Markdown) to another (like HTML).
*/
interface ConverterInterface
{
/**
* @throws CommonMarkException
* @throws ConfigurationExceptionInterface
*/
public function convert(string $input): RenderedContentInterface;
}

View file

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Node\Node;
final class Bracket
{
private Node $node;
private ?Bracket $previous;
private bool $hasNext = false;
private int $position;
private bool $image;
private bool $active = true;
public function __construct(Node $node, ?Bracket $previous, int $position, bool $image)
{
$this->node = $node;
$this->previous = $previous;
$this->position = $position;
$this->image = $image;
}
public function getNode(): Node
{
return $this->node;
}
public function getPrevious(): ?Bracket
{
return $this->previous;
}
public function hasNext(): bool
{
return $this->hasNext;
}
public function getPosition(): int
{
return $this->position;
}
public function isImage(): bool
{
return $this->image;
}
/**
* Only valid in the context of non-images (links)
*/
public function isActive(): bool
{
return $this->active;
}
/**
* @internal
*/
public function setHasNext(bool $hasNext): void
{
$this->hasNext = $hasNext;
}
/**
* @internal
*/
public function setActive(bool $active): void
{
$this->active = $active;
}
}

View file

@ -0,0 +1,134 @@
<?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;
final class Delimiter implements DelimiterInterface
{
/** @psalm-readonly */
private string $char;
/** @psalm-readonly-allow-private-mutation */
private int $length;
/** @psalm-readonly */
private int $originalLength;
/** @psalm-readonly */
private AbstractStringContainer $inlineNode;
/** @psalm-readonly-allow-private-mutation */
private ?DelimiterInterface $previous = null;
/** @psalm-readonly-allow-private-mutation */
private ?DelimiterInterface $next = null;
/** @psalm-readonly */
private bool $canOpen;
/** @psalm-readonly */
private bool $canClose;
/** @psalm-readonly-allow-private-mutation */
private bool $active;
/** @psalm-readonly */
private ?int $index = null;
public function __construct(string $char, int $numDelims, AbstractStringContainer $node, bool $canOpen, bool $canClose, ?int $index = null)
{
$this->char = $char;
$this->length = $numDelims;
$this->originalLength = $numDelims;
$this->inlineNode = $node;
$this->canOpen = $canOpen;
$this->canClose = $canClose;
$this->active = true;
$this->index = $index;
}
public function canClose(): bool
{
return $this->canClose;
}
public function canOpen(): bool
{
return $this->canOpen;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function getChar(): string
{
return $this->char;
}
public function getIndex(): ?int
{
return $this->index;
}
public function getNext(): ?DelimiterInterface
{
return $this->next;
}
public function setNext(?DelimiterInterface $next): void
{
$this->next = $next;
}
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length): void
{
$this->length = $length;
}
public function getOriginalLength(): int
{
return $this->originalLength;
}
public function getInlineNode(): AbstractStringContainer
{
return $this->inlineNode;
}
public function getPrevious(): ?DelimiterInterface
{
return $this->previous;
}
public function setPrevious(?DelimiterInterface $previous): void
{
$this->previous = $previous;
}
}

View file

@ -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;
}

View file

@ -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];
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
/**
* Special marker interface for delimiter processors that return dynamic values from getDelimiterUse()
*
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
* cache the lower bound when searching for a matching opener. This gets complicated for delimiter processors
* that use a dynamic number of characters (like with emphasis and its "multiple of 3" rule).
*/
interface CacheableDelimiterProcessorInterface extends DelimiterProcessorInterface
{
/**
* Returns a cache key of the factors that determine the number of characters to use.
*
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
* cache the lower bound when searching for a matching opener. This lower bound is usually quite simple;
* for example, with quotes, it's just the last opener with that characted. However, this gets complicated
* for delimiter processors that use a dynamic number of characters (like with emphasis and its "multiple
* of 3" rule), because the delimiter length being considered may change during processing because of that
* dynamic logic in getDelimiterUse(). Therefore, we cannot safely cache the lower bound for these dynamic
* processors without knowing the factors that determine the number of characters to use.
*
* At a minimum, this should include the delimiter character, plus any other factors used to determine
* the result of getDelimiterUse(). The format of the string is not important so long as it is unique
* (compared to other processors) and consistent for a given set of factors.
*
* If getDelimiterUse() always returns the same hard-coded value, this method should return just
* the delimiter character.
*/
public function getCacheKey(DelimiterInterface $closer): string;
}

View file

@ -0,0 +1,89 @@
<?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\Exception\InvalidArgumentException;
final class DelimiterProcessorCollection implements DelimiterProcessorCollectionInterface
{
/**
* @var array<string,DelimiterProcessorInterface>|DelimiterProcessorInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $processorsByChar = [];
public function add(DelimiterProcessorInterface $processor): void
{
$opening = $processor->getOpeningCharacter();
$closing = $processor->getClosingCharacter();
if ($opening === $closing) {
$old = $this->processorsByChar[$opening] ?? null;
if ($old !== null && $old->getOpeningCharacter() === $old->getClosingCharacter()) {
$this->addStaggeredDelimiterProcessorForChar($opening, $old, $processor);
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
}
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
$this->addDelimiterProcessorForChar($closing, $processor);
}
}
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface
{
return $this->processorsByChar[$char] ?? null;
}
/**
* @return string[]
*/
public function getDelimiterCharacters(): array
{
return \array_keys($this->processorsByChar);
}
private function addDelimiterProcessorForChar(string $delimiterChar, DelimiterProcessorInterface $processor): void
{
if (isset($this->processorsByChar[$delimiterChar])) {
throw new InvalidArgumentException(\sprintf('Delim processor for character "%s" already exists', $processor->getOpeningCharacter()));
}
$this->processorsByChar[$delimiterChar] = $processor;
}
private function addStaggeredDelimiterProcessorForChar(string $opening, DelimiterProcessorInterface $old, DelimiterProcessorInterface $new): void
{
if ($old instanceof StaggeredDelimiterProcessor) {
$s = $old;
} else {
$s = new StaggeredDelimiterProcessor($opening, $old);
}
$s->add($new);
$this->processorsByChar[$opening] = $s;
}
public function count(): int
{
return \count($this->processorsByChar);
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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\Exception\InvalidArgumentException;
interface DelimiterProcessorCollectionInterface extends \Countable
{
/**
* Add the given delim processor to the collection
*
* @param DelimiterProcessorInterface $processor The delim processor to add
*
* @throws InvalidArgumentException Exception will be thrown if attempting to add multiple processors for the same character
*/
public function add(DelimiterProcessorInterface $processor): void;
/**
* Returns the delim processor which handles the given character if one exists
*/
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface;
/**
* Returns an array of delimiter characters who have associated processors
*
* @return string[]
*/
public function getDelimiterCharacters(): array;
}

View file

@ -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;
}

View file

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* 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\Exception\InvalidArgumentException;
use League\CommonMark\Node\Inline\AbstractStringContainer;
/**
* An implementation of DelimiterProcessorInterface that dispatches all calls to two or more other DelimiterProcessors
* depending on the length of the delimiter run. All child DelimiterProcessors must have different minimum
* lengths. A given delimiter run is dispatched to the child with the largest acceptable minimum length. If no
* child is applicable, the one with the largest minimum length is chosen.
*
* @internal
*/
final class StaggeredDelimiterProcessor implements DelimiterProcessorInterface
{
/** @psalm-readonly */
private string $delimiterChar;
/** @psalm-readonly-allow-private-mutation */
private int $minLength = 0;
/**
* @var array<int, DelimiterProcessorInterface>|DelimiterProcessorInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $processors = []; // keyed by minLength in reverse order
public function __construct(string $char, DelimiterProcessorInterface $processor)
{
$this->delimiterChar = $char;
$this->add($processor);
}
public function getOpeningCharacter(): string
{
return $this->delimiterChar;
}
public function getClosingCharacter(): string
{
return $this->delimiterChar;
}
public function getMinLength(): int
{
return $this->minLength;
}
/**
* Adds the given processor to this staggered delimiter processor
*
* @throws InvalidArgumentException if attempting to add another processors for the same character and minimum length
*/
public function add(DelimiterProcessorInterface $processor): void
{
$len = $processor->getMinLength();
if (isset($this->processors[$len])) {
throw new InvalidArgumentException(\sprintf('Cannot add two delimiter processors for char "%s" and minimum length %d', $this->delimiterChar, $len));
}
$this->processors[$len] = $processor;
\krsort($this->processors);
$this->minLength = \min($this->minLength, $len);
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
return $this->findProcessor($opener->getLength())->getDelimiterUse($opener, $closer);
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
{
$this->findProcessor($delimiterUse)->process($opener, $closer, $delimiterUse);
}
private function findProcessor(int $len): DelimiterProcessorInterface
{
// Find the "longest" processor which can handle this length
foreach ($this->processors as $processor) {
if ($processor->getMinLength() <= $len) {
return $processor;
}
}
// Just use the first one in our list
$first = \reset($this->processors);
\assert($first instanceof DelimiterProcessorInterface);
return $first;
}
}

View file

@ -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),
]),
]);
}
}

View file

@ -0,0 +1,19 @@
<?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\Environment;
interface EnvironmentAwareInterface
{
public function setEnvironment(EnvironmentInterface $environment): void;
}

View file

@ -0,0 +1,97 @@
<?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\Environment;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Exception\AlreadyInitializedException;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\Config\ConfigurationProviderInterface;
/**
* Interface for building the Environment with any extensions, parsers, listeners, etc. that it may need
*/
interface EnvironmentBuilderInterface extends ConfigurationProviderInterface
{
/**
* Registers the given extension with the Environment
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface;
/**
* Registers the given block start parser with the Environment
*
* @param BlockStartParserInterface $parser Block parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given inline parser with the Environment
*
* @param InlineParserInterface $parser Inline parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given delimiter processor with the Environment
*
* @param DelimiterProcessorInterface $processor Delimiter processors instance
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface;
/**
* Registers the given node renderer with the Environment
*
* @param string $nodeClass The fully-qualified node element class name the renderer below should handle
* @param NodeRendererInterface $renderer The renderer responsible for rendering the type of element given above
* @param int $priority Priority (a higher number will be executed earlier)
*
* @psalm-param class-string<Node> $nodeClass
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given event listener
*
* @param class-string $eventClass Fully-qualified class name of the event this listener should respond to
* @param callable $listener Listener to be executed
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface;
}

View file

@ -0,0 +1,55 @@
<?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\Environment;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Normalizer\TextNormalizerInterface;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\Config\ConfigurationProviderInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
interface EnvironmentInterface extends ConfigurationProviderInterface, EventDispatcherInterface
{
/**
* Get all registered extensions
*
* @return ExtensionInterface[]
*/
public function getExtensions(): iterable;
/**
* @return iterable<BlockStartParserInterface>
*/
public function getBlockStartParsers(): iterable;
/**
* @return iterable<InlineParserInterface>
*/
public function getInlineParsers(): iterable;
public function getDelimiterProcessors(): DelimiterProcessorCollection;
/**
* @psalm-param class-string<Node> $nodeClass
*
* @return iterable<NodeRendererInterface>
*/
public function getRenderersForClass(string $nodeClass): iterable;
public function getSlugNormalizer(): TextNormalizerInterface;
}

View file

@ -0,0 +1,54 @@
<?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 Symfony EventDispatcher "Event" contract
* - (c) 2018-2019 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*/
abstract class AbstractEvent implements StoppableEventInterface
{
/** @psalm-readonly-allow-private-mutation */
private bool $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
final public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
final public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}

View file

@ -0,0 +1,35 @@
<?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\Event;
use League\CommonMark\Node\Block\Document;
/**
* Event dispatched when the document has been fully parsed
*/
final class DocumentParsedEvent extends AbstractEvent
{
/** @psalm-readonly */
private Document $document;
public function __construct(Document $document)
{
$this->document = $document;
}
public function getDocument(): Document
{
return $this->document;
}
}

View file

@ -0,0 +1,49 @@
<?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\Event;
use League\CommonMark\Input\MarkdownInputInterface;
use League\CommonMark\Node\Block\Document;
/**
* Event dispatched when the document is about to be parsed
*/
final class DocumentPreParsedEvent extends AbstractEvent
{
/** @psalm-readonly */
private Document $document;
private MarkdownInputInterface $markdown;
public function __construct(Document $document, MarkdownInputInterface $markdown)
{
$this->document = $document;
$this->markdown = $markdown;
}
public function getDocument(): Document
{
return $this->document;
}
public function getMarkdown(): MarkdownInputInterface
{
return $this->markdown;
}
public function replaceMarkdown(MarkdownInputInterface $markdownInput): void
{
$this->markdown = $markdownInput;
}
}

View file

@ -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>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use League\CommonMark\Node\Block\Document;
/**
* Event dispatched just before rendering begins
*/
final class DocumentPreRenderEvent extends AbstractEvent
{
/** @psalm-readonly */
private Document $document;
/** @psalm-readonly */
private string $format;
public function __construct(Document $document, string $format)
{
$this->document = $document;
$this->format = $format;
}
public function getDocument(): Document
{
return $this->document;
}
public function getFormat(): string
{
return $this->format;
}
}

View file

@ -0,0 +1,42 @@
<?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\Event;
use League\CommonMark\Output\RenderedContentInterface;
final class DocumentRenderedEvent extends AbstractEvent
{
private RenderedContentInterface $output;
public function __construct(RenderedContentInterface $output)
{
$this->output = $output;
}
/**
* @psalm-mutation-free
*/
public function getOutput(): RenderedContentInterface
{
return $this->output;
}
/**
* @psalm-external-mutation-free
*/
public function replaceOutput(RenderedContentInterface $output): void
{
$this->output = $output;
}
}

View file

@ -0,0 +1,50 @@
<?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\Event;
/**
* @internal
*
* @psalm-immutable
*/
final class ListenerData
{
/** @var class-string */
private string $event;
/** @var callable */
private $listener;
/**
* @param class-string $event
*/
public function __construct(string $event, callable $listener)
{
$this->event = $event;
$this->listener = $listener;
}
/**
* @return class-string
*/
public function getEvent(): string
{
return $this->event;
}
public function getListener(): callable
{
return $this->listener;
}
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
class AlreadyInitializedException extends LogicException implements CommonMarkException
{
}

View file

@ -0,0 +1,21 @@
<?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\Exception;
/**
* Marker interface for all exceptions thrown by this library.
*/
interface CommonMarkException extends \Throwable
{
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
class IOException extends \RuntimeException implements CommonMarkException
{
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements CommonMarkException
{
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
class LogicException extends \LogicException implements CommonMarkException
{
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
class MissingDependencyException extends \RuntimeException implements CommonMarkException
{
}

View file

@ -0,0 +1,18 @@
<?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\Exception;
final class UnexpectedEncodingException extends \RuntimeException implements CommonMarkException
{
}

View file

@ -0,0 +1,32 @@
<?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;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockStartParser;
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
use League\CommonMark\Extension\ExtensionInterface;
final class AttributesExtension implements ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addBlockStartParser(new AttributesBlockStartParser());
$environment->addInlineParser(new AttributesInlineParser());
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
}
}

View file

@ -0,0 +1,139 @@
<?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\Event;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Node;
final class AttributesListener
{
private const DIRECTION_PREFIX = 'prefix';
private const DIRECTION_SUFFIX = 'suffix';
public function processDocument(DocumentParsedEvent $event): void
{
foreach ($event->getDocument()->iterator() as $node) {
if (! ($node instanceof Attributes || $node instanceof AttributesInline)) {
continue;
}
[$target, $direction] = self::findTargetAndDirection($node);
if ($target instanceof Node) {
$parent = $target->parent();
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
$target = $parent;
}
if ($direction === self::DIRECTION_SUFFIX) {
$attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes());
} else {
$attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
}
$target->data->set('attributes', $attributes);
}
$node->detach();
}
}
/**
* @param Attributes|AttributesInline $node
*
* @return array<Node|string|null>
*/
private static function findTargetAndDirection($node): array
{
$target = null;
$direction = null;
$previous = $next = $node;
while (true) {
$previous = self::getPrevious($previous);
$next = self::getNext($next);
if ($previous === null && $next === null) {
if (! $node->parent() instanceof FencedCode) {
$target = $node->parent();
$direction = self::DIRECTION_SUFFIX;
}
break;
}
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
continue;
}
if ($previous !== null && ! self::isAttributesNode($previous)) {
$target = $previous;
$direction = self::DIRECTION_SUFFIX;
break;
}
if ($next !== null && ! self::isAttributesNode($next)) {
$target = $next;
$direction = self::DIRECTION_PREFIX;
break;
}
}
return [$target, $direction];
}
/**
* Get any previous block (sibling or parent) this might apply to
*/
private static function getPrevious(?Node $node = null): ?Node
{
if ($node instanceof Attributes) {
if ($node->getTarget() === Attributes::TARGET_NEXT) {
return null;
}
if ($node->getTarget() === Attributes::TARGET_PARENT) {
return $node->parent();
}
}
return $node instanceof Node ? $node->previous() : null;
}
/**
* Get any previous block (sibling or parent) this might apply to
*/
private static function getNext(?Node $node = null): ?Node
{
if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) {
return null;
}
return $node instanceof Node ? $node->next() : null;
}
private static function isAttributesNode(Node $node): bool
{
return $node instanceof Attributes || $node instanceof AttributesInline;
}
}

View file

@ -0,0 +1,65 @@
<?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\Node;
use League\CommonMark\Node\Block\AbstractBlock;
final class Attributes extends AbstractBlock
{
public const TARGET_PARENT = 0;
public const TARGET_PREVIOUS = 1;
public const TARGET_NEXT = 2;
/** @var array<string, mixed> */
private array $attributes;
private int $target = self::TARGET_NEXT;
/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes)
{
parent::__construct();
$this->attributes = $attributes;
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @param array<string, mixed> $attributes
*/
public function setAttributes(array $attributes): void
{
$this->attributes = $attributes;
}
public function getTarget(): int
{
return $this->target;
}
public function setTarget(int $target): void
{
$this->target = $target;
}
}

View file

@ -0,0 +1,57 @@
<?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\Node;
use League\CommonMark\Node\Inline\AbstractInline;
final class AttributesInline extends AbstractInline
{
/** @var array<string, mixed> */
private array $attributes;
private bool $block;
/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes, bool $block)
{
parent::__construct();
$this->attributes = $attributes;
$this->block = $block;
}
/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @param array<string, mixed> $attributes
*/
public function setAttributes(array $attributes): void
{
$this->attributes = $attributes;
}
public function isBlock(): bool
{
return $this->block;
}
}

View file

@ -0,0 +1,92 @@
<?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\Parser;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
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 AttributesBlockContinueParser extends AbstractBlockContinueParser
{
private Attributes $block;
private AbstractBlock $container;
private bool $hasSubsequentLine = false;
/**
* @param array<string, mixed> $attributes The attributes identified by the block start parser
* @param AbstractBlock $container The node we were in when these attributes were discovered
*/
public function __construct(array $attributes, AbstractBlock $container)
{
$this->block = new Attributes($attributes);
$this->container = $container;
}
public function getBlock(): AbstractBlock
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
$this->hasSubsequentLine = true;
$cursor->advanceToNextNonSpaceOrTab();
// Does this next line also have attributes?
$attributes = AttributesHelper::parseAttributes($cursor);
$cursor->advanceToNextNonSpaceOrTab();
if ($cursor->isAtEnd() && $attributes !== []) {
// It does! Merge them into what we parsed previously
$this->block->setAttributes(AttributesHelper::mergeAttributes(
$this->block->getAttributes(),
$attributes
));
// Tell the core parser we've consumed everything
return BlockContinue::at($cursor);
}
// Okay, so there are no attributes on the next line
// If this next line is blank we know we can't target the next node, it must be a previous one
if ($cursor->isBlank()) {
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
}
return BlockContinue::none();
}
public function closeBlock(): void
{
// Attributes appearing at the very end of the document won't have any last lines to check
// so we can make that determination here
if (! $this->hasSubsequentLine) {
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
}
// We know this block must apply to the "previous" block, but that could be a sibling or parent,
// so we check the containing block to see which one it might be.
if ($this->block->getTarget() === Attributes::TARGET_PREVIOUS && $this->block->parent() === $this->container) {
$this->block->setTarget(Attributes::TARGET_PARENT);
}
}
}

View file

@ -0,0 +1,40 @@
<?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\Parser;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class AttributesBlockStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
$originalPosition = $cursor->getPosition();
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === [] && $originalPosition === $cursor->getPosition()) {
return BlockStart::none();
}
if ($cursor->getNextNonSpaceCharacter() !== null) {
return BlockStart::none();
}
return BlockStart::of(new AttributesBlockContinueParser($attributes, $parserState->getActiveBlockParser()->getBlock()))->at($cursor);
}
}

View file

@ -0,0 +1,54 @@
<?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\Parser;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
use League\CommonMark\Node\StringContainerInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
final class AttributesInlineParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('{');
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$char = (string) $cursor->peek(-1);
$attributes = AttributesHelper::parseAttributes($cursor);
if ($attributes === []) {
return false;
}
if ($char === ' ' && ($prev = $inlineContext->getContainer()->lastChild()) instanceof StringContainerInterface) {
$prev->setLiteral(\rtrim($prev->getLiteral(), ' '));
}
if ($char === '') {
$cursor->advanceToNextNonSpaceOrNewline();
}
$node = new AttributesInline($attributes, $char === ' ' || $char === '');
$inlineContext->getContainer()->appendChild($node);
return true;
}
}

View file

@ -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;
}
}

View file

@ -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'),
));
}
}

View file

@ -0,0 +1,48 @@
<?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 EmailAutolinkParser implements InlineParserInterface
{
private const REGEX = '[A-Za-z0-9.\-_+]+@[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.]+';
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(self::REGEX);
}
public function parse(InlineParserContext $inlineContext): bool
{
$email = $inlineContext->getFullMatch();
// The last character cannot be - or _
if (\in_array(\substr($email, -1), ['-', '_'], true)) {
return false;
}
// Does the URL end with punctuation that should be stripped?
if (\substr($email, -1) === '.') {
$email = \substr($email, 0, -1);
}
$inlineContext->getCursor()->advanceBy(\strlen($email));
$inlineContext->getContainer()->appendChild(new Link('mailto:' . $email, $email));
return true;
}
}

View file

@ -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 dont 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['('];
}
}

View file

@ -0,0 +1,92 @@
<?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;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\CommonMark\Delimiter\Processor\EmphasisDelimiterProcessor;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Node as CoreNode;
use League\CommonMark\Parser as CoreParser;
use League\CommonMark\Renderer as CoreRenderer;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;
final class CommonMarkCoreExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('commonmark', Expect::structure([
'use_asterisk' => Expect::bool(true),
'use_underscore' => Expect::bool(true),
'enable_strong' => Expect::bool(true),
'enable_em' => Expect::bool(true),
'unordered_list_markers' => Expect::listOf('string')->min(1)->default(['*', '+', '-'])->mergeDefaults(false),
]));
}
// phpcs:disable Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma,Squiz.WhiteSpace.SemicolonSpacing.Incorrect
public function register(EnvironmentBuilderInterface $environment): void
{
$environment
->addBlockStartParser(new Parser\Block\BlockQuoteStartParser(), 70)
->addBlockStartParser(new Parser\Block\HeadingStartParser(), 60)
->addBlockStartParser(new Parser\Block\FencedCodeStartParser(), 50)
->addBlockStartParser(new Parser\Block\HtmlBlockStartParser(), 40)
->addBlockStartParser(new Parser\Block\ThematicBreakStartParser(), 20)
->addBlockStartParser(new Parser\Block\ListBlockStartParser(), 10)
->addBlockStartParser(new Parser\Block\IndentedCodeStartParser(), -100)
->addInlineParser(new CoreParser\Inline\NewlineParser(), 200)
->addInlineParser(new Parser\Inline\BacktickParser(), 150)
->addInlineParser(new Parser\Inline\EscapableParser(), 80)
->addInlineParser(new Parser\Inline\EntityParser(), 70)
->addInlineParser(new Parser\Inline\AutolinkParser(), 50)
->addInlineParser(new Parser\Inline\HtmlInlineParser(), 40)
->addInlineParser(new Parser\Inline\CloseBracketParser(), 30)
->addInlineParser(new Parser\Inline\OpenBracketParser(), 20)
->addInlineParser(new Parser\Inline\BangParser(), 10)
->addRenderer(Node\Block\BlockQuote::class, new Renderer\Block\BlockQuoteRenderer(), 0)
->addRenderer(CoreNode\Block\Document::class, new CoreRenderer\Block\DocumentRenderer(), 0)
->addRenderer(Node\Block\FencedCode::class, new Renderer\Block\FencedCodeRenderer(), 0)
->addRenderer(Node\Block\Heading::class, new Renderer\Block\HeadingRenderer(), 0)
->addRenderer(Node\Block\HtmlBlock::class, new Renderer\Block\HtmlBlockRenderer(), 0)
->addRenderer(Node\Block\IndentedCode::class, new Renderer\Block\IndentedCodeRenderer(), 0)
->addRenderer(Node\Block\ListBlock::class, new Renderer\Block\ListBlockRenderer(), 0)
->addRenderer(Node\Block\ListItem::class, new Renderer\Block\ListItemRenderer(), 0)
->addRenderer(CoreNode\Block\Paragraph::class, new CoreRenderer\Block\ParagraphRenderer(), 0)
->addRenderer(Node\Block\ThematicBreak::class, new Renderer\Block\ThematicBreakRenderer(), 0)
->addRenderer(Node\Inline\Code::class, new Renderer\Inline\CodeRenderer(), 0)
->addRenderer(Node\Inline\Emphasis::class, new Renderer\Inline\EmphasisRenderer(), 0)
->addRenderer(Node\Inline\HtmlInline::class, new Renderer\Inline\HtmlInlineRenderer(), 0)
->addRenderer(Node\Inline\Image::class, new Renderer\Inline\ImageRenderer(), 0)
->addRenderer(Node\Inline\Link::class, new Renderer\Inline\LinkRenderer(), 0)
->addRenderer(CoreNode\Inline\Newline::class, new CoreRenderer\Inline\NewlineRenderer(), 0)
->addRenderer(Node\Inline\Strong::class, new Renderer\Inline\StrongRenderer(), 0)
->addRenderer(CoreNode\Inline\Text::class, new CoreRenderer\Inline\TextRenderer(), 0)
;
if ($environment->getConfiguration()->get('commonmark/use_asterisk')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('*'));
}
if ($environment->getConfiguration()->get('commonmark/use_underscore')) {
$environment->addDelimiterProcessor(new EmphasisDelimiterProcessor('_'));
}
}
}

View file

@ -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(),
);
}
}

View file

@ -0,0 +1,20 @@
<?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\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
class BlockQuote extends AbstractBlock
{
}

View file

@ -0,0 +1,100 @@
<?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\StringContainerInterface;
final class FencedCode extends AbstractBlock implements StringContainerInterface
{
private ?string $info = null;
private string $literal = '';
private int $length;
private string $char;
private int $offset;
public function __construct(int $length, string $char, int $offset)
{
parent::__construct();
$this->length = $length;
$this->char = $char;
$this->offset = $offset;
}
public function getInfo(): ?string
{
return $this->info;
}
/**
* @return string[]
*/
public function getInfoWords(): array
{
return \preg_split('/\s+/', $this->info ?? '') ?: [];
}
public function setInfo(string $info): void
{
$this->info = $info;
}
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
public function getChar(): string
{
return $this->char;
}
public function setChar(string $char): void
{
$this->char = $char;
}
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length): void
{
$this->length = $length;
}
public function getOffset(): int
{
return $this->offset;
}
public function setOffset(int $offset): void
{
$this->offset = $offset;
}
}

View file

@ -0,0 +1,41 @@
<?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;
final class Heading extends AbstractBlock
{
private int $level;
public function __construct(int $level)
{
parent::__construct();
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel(int $level): void
{
$this->level = $level;
}
}

View file

@ -0,0 +1,79 @@
<?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\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\RawMarkupContainerInterface;
final class HtmlBlock extends AbstractBlock implements RawMarkupContainerInterface
{
// Any changes to these constants should be reflected in .phpstorm.meta.php
public const TYPE_1_CODE_CONTAINER = 1;
public const TYPE_2_COMMENT = 2;
public const TYPE_3 = 3;
public const TYPE_4 = 4;
public const TYPE_5_CDATA = 5;
public const TYPE_6_BLOCK_ELEMENT = 6;
public const TYPE_7_MISC_ELEMENT = 7;
/**
* @psalm-var self::TYPE_* $type
* @phpstan-var self::TYPE_* $type
*/
private int $type;
private string $literal = '';
/**
* @psalm-param self::TYPE_* $type
*
* @phpstan-param self::TYPE_* $type
*/
public function __construct(int $type)
{
parent::__construct();
$this->type = $type;
}
/**
* @psalm-return self::TYPE_*
*
* @phpstan-return self::TYPE_*
*/
public function getType(): int
{
return $this->type;
}
/**
* @psalm-param self::TYPE_* $type
*
* @phpstan-param self::TYPE_* $type
*/
public function setType(int $type): void
{
$this->type = $type;
}
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
}

View file

@ -0,0 +1,32 @@
<?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\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Node\StringContainerInterface;
final class IndentedCode extends AbstractBlock implements StringContainerInterface
{
private string $literal = '';
public function getLiteral(): string
{
return $this->literal;
}
public function setLiteral(string $literal): void
{
$this->literal = $literal;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,47 @@
<?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;
class ListData
{
public ?int $start = null;
public int $padding = 0;
/**
* @psalm-var ListBlock::TYPE_*
* @phpstan-var ListBlock::TYPE_*
*/
public string $type;
/**
* @psalm-var ListBlock::DELIM_*|null
* @phpstan-var ListBlock::DELIM_*|null
*/
public ?string $delimiter = null;
public ?string $bulletChar = null;
public int $markerOffset;
public function equals(ListData $data): bool
{
return $this->type === $data->type &&
$this->delimiter === $data->delimiter &&
$this->bulletChar === $data->bulletChar;
}
}

View file

@ -0,0 +1,37 @@
<?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;
class ListItem extends AbstractBlock
{
/** @psalm-readonly */
protected ListData $listData;
public function __construct(ListData $listData)
{
parent::__construct();
$this->listData = $listData;
}
public function getListData(): ListData
{
return $this->listData;
}
}

View file

@ -0,0 +1,20 @@
<?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\Node\Block;
use League\CommonMark\Node\Block\AbstractBlock;
class ThematicBreak extends AbstractBlock
{
}

View file

@ -0,0 +1,41 @@
<?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\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
abstract class AbstractWebResource extends AbstractInline
{
protected string $url;
public function __construct(string $url)
{
parent::__construct();
$this->url = $url;
}
public function getUrl(): string
{
return $this->url;
}
public function setUrl(string $url): void
{
$this->url = $url;
}
}

View file

@ -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\Extension\CommonMark\Node\Inline;
use League\CommonMark\Node\Inline\AbstractStringContainer;
class Code extends AbstractStringContainer
{
}

View file

@ -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\Node\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Inline\DelimitedInterface;
final class Emphasis extends AbstractInline implements DelimitedInterface
{
private string $delimiter;
public function __construct(string $delimiter = '_')
{
parent::__construct();
$this->delimiter = $delimiter;
}
public function getOpeningDelimiter(): string
{
return $this->delimiter;
}
public function getClosingDelimiter(): string
{
return $this->delimiter;
}
}

View file

@ -0,0 +1,24 @@
<?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\Inline;
use League\CommonMark\Node\Inline\AbstractStringContainer;
use League\CommonMark\Node\RawMarkupContainerInterface;
final class HtmlInline extends AbstractStringContainer implements RawMarkupContainerInterface
{
}

View file

@ -0,0 +1,49 @@
<?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\Inline;
use League\CommonMark\Node\Inline\Text;
class Image extends AbstractWebResource
{
protected ?string $title = null;
public function __construct(string $url, ?string $label = null, ?string $title = null)
{
parent::__construct($url);
if ($label !== null && $label !== '') {
$this->appendChild(new Text($label));
}
$this->title = $title;
}
public function getTitle(): ?string
{
if ($this->title === '') {
return null;
}
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
}

View file

@ -0,0 +1,49 @@
<?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\Inline;
use League\CommonMark\Node\Inline\Text;
class Link extends AbstractWebResource
{
protected ?string $title = null;
public function __construct(string $url, ?string $label = null, ?string $title = null)
{
parent::__construct($url);
if ($label !== null && $label !== '') {
$this->appendChild(new Text($label));
}
$this->title = $title;
}
public function getTitle(): ?string
{
if ($this->title === '') {
return null;
}
return $this->title;
}
public function setTitle(?string $title): void
{
$this->title = $title;
}
}

View file

@ -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\Node\Inline;
use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Inline\DelimitedInterface;
final class Strong extends AbstractInline implements DelimitedInterface
{
private string $delimiter;
public function __construct(string $delimiter = '**')
{
parent::__construct();
$this->delimiter = $delimiter;
}
public function getOpeningDelimiter(): string
{
return $this->delimiter;
}
public function getClosingDelimiter(): string
{
return $this->delimiter;
}
}

View file

@ -0,0 +1,60 @@
<?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\BlockQuote;
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 BlockQuoteParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private BlockQuote $block;
public function __construct()
{
$this->block = new BlockQuote();
}
public function getBlock(): BlockQuote
{
return $this->block;
}
public function isContainer(): bool
{
return true;
}
public function canContain(AbstractBlock $childBlock): bool
{
return true;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if (! $cursor->isIndented() && $cursor->getNextNonSpaceCharacter() === '>') {
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return BlockContinue::at($cursor);
}
return BlockContinue::none();
}
}

View file

@ -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\CommonMark\Parser\Block;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class BlockQuoteStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
if ($cursor->getNextNonSpaceCharacter() !== '>') {
return BlockStart::none();
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(1);
$cursor->advanceBySpaceOrTab();
return BlockStart::of(new BlockQuoteParser())->at($cursor);
}
}

View file

@ -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");
}
}
}

View file

@ -0,0 +1,40 @@
<?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\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class FencedCodeStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['`', '~'], true)) {
return BlockStart::none();
}
$indent = $cursor->getIndent();
$fence = $cursor->match('/^[ \t]*(?:`{3,}(?!.*`)|~{3,})/');
if ($fence === null) {
return BlockStart::none();
}
// fenced code block
$fence = \ltrim($fence, " \t");
return BlockStart::of(new FencedCodeParser(\strlen($fence), $fence[0], $indent))->at($cursor);
}
}

View file

@ -0,0 +1,51 @@
<?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\Heading;
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;
final class HeadingParser extends AbstractBlockContinueParser implements BlockContinueParserWithInlinesInterface
{
/** @psalm-readonly */
private Heading $block;
private string $content;
public function __construct(int $level, string $content)
{
$this->block = new Heading($level);
$this->content = $content;
}
public function getBlock(): Heading
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
return BlockContinue::none();
}
public function parseInlines(InlineParserEngineInterface $inlineParser): void
{
$inlineParser->parse($this->content, $this->block);
}
}

View file

@ -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\Extension\CommonMark\Parser\Block;
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;
class HeadingStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || ! \in_array($cursor->getNextNonSpaceCharacter(), ['#', '-', '='], true)) {
return BlockStart::none();
}
$cursor->advanceToNextNonSpaceOrTab();
if ($atxHeading = self::getAtxHeader($cursor)) {
return BlockStart::of($atxHeading)->at($cursor);
}
$setextHeadingLevel = self::getSetextHeadingLevel($cursor);
if ($setextHeadingLevel > 0) {
$content = $parserState->getParagraphContent();
if ($content !== null) {
$cursor->advanceToEnd();
return BlockStart::of(new HeadingParser($setextHeadingLevel, $content))
->at($cursor)
->replaceActiveBlockParser();
}
}
return BlockStart::none();
}
private static function getAtxHeader(Cursor $cursor): ?HeadingParser
{
$match = RegexHelper::matchFirst('/^#{1,6}(?:[ \t]+|$)/', $cursor->getRemainder());
if (! $match) {
return null;
}
$cursor->advanceToNextNonSpaceOrTab();
$cursor->advanceBy(\strlen($match[0]));
$level = \strlen(\trim($match[0]));
$str = $cursor->getRemainder();
$str = \preg_replace('/^[ \t]*#+[ \t]*$/', '', $str);
\assert(\is_string($str));
$str = \preg_replace('/[ \t]+#+[ \t]*$/', '', $str);
\assert(\is_string($str));
return new HeadingParser($level, $str);
}
private static function getSetextHeadingLevel(Cursor $cursor): int
{
$match = RegexHelper::matchFirst('/^(?:=+|-+)[ \t]*$/', $cursor->getRemainder());
if ($match === null) {
return 0;
}
return $match[0][0] === '=' ? 1 : 2;
}
}

View file

@ -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\HtmlBlock;
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\RegexHelper;
final class HtmlBlockParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private HtmlBlock $block;
private string $content = '';
private bool $finished = false;
/**
* @psalm-param HtmlBlock::TYPE_* $blockType
*
* @phpstan-param HtmlBlock::TYPE_* $blockType
*/
public function __construct(int $blockType)
{
$this->block = new HtmlBlock($blockType);
}
public function getBlock(): HtmlBlock
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
if ($this->finished) {
return BlockContinue::none();
}
if ($cursor->isBlank() && \in_array($this->block->getType(), [HtmlBlock::TYPE_6_BLOCK_ELEMENT, HtmlBlock::TYPE_7_MISC_ELEMENT], true)) {
return BlockContinue::none();
}
return BlockContinue::at($cursor);
}
public function addLine(string $line): void
{
if ($this->content !== '') {
$this->content .= "\n";
}
$this->content .= $line;
// Check for end condition
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
if ($this->block->getType() <= HtmlBlock::TYPE_5_CDATA) {
if (\preg_match(RegexHelper::getHtmlBlockCloseRegex($this->block->getType()), $line) === 1) {
$this->finished = true;
}
}
}
public function closeBlock(): void
{
$this->block->setLiteral($this->content);
$this->content = '';
}
}

View file

@ -0,0 +1,62 @@
<?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\HtmlBlock;
use League\CommonMark\Node\Block\Paragraph;
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;
final class HtmlBlockStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented() || $cursor->getNextNonSpaceCharacter() !== '<') {
return BlockStart::none();
}
$tmpCursor = clone $cursor;
$tmpCursor->advanceToNextNonSpaceOrTab();
$line = $tmpCursor->getRemainder();
for ($blockType = 1; $blockType <= 7; $blockType++) {
/** @psalm-var HtmlBlock::TYPE_* $blockType */
/** @phpstan-var HtmlBlock::TYPE_* $blockType */
$match = RegexHelper::matchAt(
RegexHelper::getHtmlBlockOpenRegex($blockType),
$line
);
if ($match !== null && ($blockType < 7 || $this->isType7BlockAllowed($cursor, $parserState))) {
return BlockStart::of(new HtmlBlockParser($blockType))->at($cursor);
}
}
return BlockStart::none();
}
private function isType7BlockAllowed(Cursor $cursor, MarkdownParserStateInterface $parserState): bool
{
// Type 7 blocks can't interrupt paragraphs
if ($parserState->getLastMatchedBlockParser()->getBlock() instanceof Paragraph) {
return false;
}
// Even lazy ones
return ! $parserState->getActiveBlockParser()->canHaveLazyContinuationLines();
}
}

View file

@ -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);
}
}

View file

@ -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>
*
* 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\Node\Block\Paragraph;
use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
final class IndentedCodeStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if (! $cursor->isIndented()) {
return BlockStart::none();
}
if ($parserState->getActiveBlockParser()->getBlock() instanceof Paragraph) {
return BlockStart::none();
}
if ($cursor->isBlank()) {
return BlockStart::none();
}
$cursor->advanceBy(Cursor::INDENT_LEVEL, true);
return BlockStart::of(new IndentedCodeParser())->at($cursor);
}
}

View file

@ -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;
}
}

View file

@ -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), '/') . ']/';
}
}

View file

@ -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());
}
}
}

View file

@ -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>
*
* 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\ThematicBreak;
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 ThematicBreakParser extends AbstractBlockContinueParser
{
/** @psalm-readonly */
private ThematicBreak $block;
public function __construct()
{
$this->block = new ThematicBreak();
}
public function getBlock(): ThematicBreak
{
return $this->block;
}
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
{
// a horizontal rule can never container > 1 line, so fail to match
return BlockContinue::none();
}
}

View file

@ -0,0 +1,40 @@
<?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\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;
use League\CommonMark\Util\RegexHelper;
final class ThematicBreakStartParser implements BlockStartParserInterface
{
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
{
if ($cursor->isIndented()) {
return BlockStart::none();
}
$match = RegexHelper::matchAt(RegexHelper::REGEX_THEMATIC_BREAK, $cursor->getLine(), $cursor->getNextNonSpacePosition());
if ($match === null) {
return BlockStart::none();
}
// Advance to the end of the string, consuming the entire line (of the thematic break)
$cursor->advanceToEnd();
return BlockStart::of(new ThematicBreakParser())->at($cursor);
}
}

View file

@ -0,0 +1,54 @@
<?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\Link;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\UrlEncoder;
final class AutolinkParser implements InlineParserInterface
{
private const EMAIL_REGEX = '<([a-zA-Z0-9.!#$%&\'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>';
private const OTHER_LINK_REGEX = '<([A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*)>';
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(self::EMAIL_REGEX . '|' . self::OTHER_LINK_REGEX);
}
public function parse(InlineParserContext $inlineContext): bool
{
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$matches = $inlineContext->getMatches();
if ($matches[1] !== '') {
$inlineContext->getContainer()->appendChild(new Link('mailto:' . UrlEncoder::unescapeAndEncode($matches[1]), $matches[1]));
return true;
}
if ($matches[2] !== '') {
$inlineContext->getContainer()->appendChild(new Link(UrlEncoder::unescapeAndEncode($matches[2]), $matches[2]));
return true;
}
return false; // This should never happen
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
use League\CommonMark\Util\Html5EntityDecoder;
use League\CommonMark\Util\RegexHelper;
final class EntityParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(RegexHelper::PARTIAL_ENTITY);
}
public function parse(InlineParserContext $inlineContext): bool
{
$entity = $inlineContext->getFullMatch();
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$inlineContext->getContainer()->appendChild(new Text(Html5EntityDecoder::decode($entity)));
return true;
}
}

View file

@ -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>
*
* 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\Newline;
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;
final class EscapableParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::string('\\');
}
public function parse(InlineParserContext $inlineContext): bool
{
$cursor = $inlineContext->getCursor();
$nextChar = $cursor->peek();
if ($nextChar === "\n") {
$cursor->advanceBy(2);
$inlineContext->getContainer()->appendChild(new Newline(Newline::HARDBREAK));
return true;
}
if ($nextChar !== null && RegexHelper::isEscapable($nextChar)) {
$cursor->advanceBy(2);
$inlineContext->getContainer()->appendChild(new Text($nextChar));
return true;
}
$cursor->advanceBy(1);
$inlineContext->getContainer()->appendChild(new Text('\\'));
return true;
}
}

View file

@ -0,0 +1,41 @@
<?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\HtmlInline;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
final class HtmlInlineParser implements InlineParserInterface
{
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::regex(RegexHelper::PARTIAL_HTMLTAG)->caseSensitive();
}
public function parse(InlineParserContext $inlineContext): bool
{
$inline = $inlineContext->getFullMatch();
$inlineContext->getCursor()->advanceBy($inlineContext->getFullMatchLength());
$inlineContext->getContainer()->appendChild(new HtmlInline($inline));
return true;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,70 @@
<?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\BlockQuote;
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 BlockQuoteRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param BlockQuote $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
BlockQuote::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$filling = $childRenderer->renderNodes($node->children());
$innerSeparator = $childRenderer->getInnerSeparator();
if ($filling === '') {
return new HtmlElement('blockquote', $attrs, $innerSeparator);
}
return new HtmlElement(
'blockquote',
$attrs,
$innerSeparator . $filling . $innerSeparator
);
}
public function getXmlTagName(Node $node): string
{
return 'block_quote';
}
/**
* @param BlockQuote $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -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
*
* 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\FencedCode;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class FencedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param FencedCode $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
FencedCode::assertInstanceOf($node);
$attrs = $node->data->getData('attributes');
$infoWords = $node->getInfoWords();
if (\count($infoWords) !== 0 && $infoWords[0] !== '') {
$class = $infoWords[0];
if (! \str_starts_with($class, 'language-')) {
$class = 'language-' . $class;
}
$attrs->append('class', $class);
}
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs->export(), Xml::escape($node->getLiteral()))
);
}
public function getXmlTagName(Node $node): string
{
return 'code_block';
}
/**
* @param FencedCode $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
FencedCode::assertInstanceOf($node);
if (($info = $node->getInfo()) === null || $info === '') {
return [];
}
return ['info' => $info];
}
}

View file

@ -0,0 +1,64 @@
<?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\Heading;
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 HeadingRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Heading $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Heading::assertInstanceOf($node);
$tag = 'h' . $node->getLevel();
$attrs = $node->data->get('attributes');
return new HtmlElement($tag, $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'heading';
}
/**
* @param Heading $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Heading::assertInstanceOf($node);
return ['level' => $node->getLevel()];
}
}

View file

@ -0,0 +1,66 @@
<?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\HtmlBlock;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlFilter;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class HtmlBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param HtmlBlock $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
{
HtmlBlock::assertInstanceOf($node);
$htmlInput = $this->config->get('html_input');
return HtmlFilter::filter($node->getLiteral(), $htmlInput);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'html_block';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -0,0 +1,61 @@
<?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\IndentedCode;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class IndentedCodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param IndentedCode $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
IndentedCode::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement(
'pre',
[],
new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()))
);
}
public function getXmlTagName(Node $node): string
{
return 'code_block';
}
/**
* @return array<string, scalar>
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -0,0 +1,86 @@
<?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\ListBlock;
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 ListBlockRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param ListBlock $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
ListBlock::assertInstanceOf($node);
$listData = $node->getListData();
$tag = $listData->type === ListBlock::TYPE_BULLET ? 'ul' : 'ol';
$attrs = $node->data->get('attributes');
if ($listData->start !== null && $listData->start !== 1) {
$attrs['start'] = (string) $listData->start;
}
$innerSeparator = $childRenderer->getInnerSeparator();
return new HtmlElement($tag, $attrs, $innerSeparator . $childRenderer->renderNodes($node->children()) . $innerSeparator);
}
public function getXmlTagName(Node $node): string
{
return 'list';
}
/**
* @param ListBlock $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
ListBlock::assertInstanceOf($node);
$data = $node->getListData();
if ($data->type === ListBlock::TYPE_BULLET) {
return [
'type' => $data->type,
'tight' => $node->isTight() ? 'true' : 'false',
];
}
return [
'type' => $data->type,
'start' => $data->start ?? 1,
'tight' => $node->isTight(),
'delimiter' => $data->delimiter ?? ListBlock::DELIM_PERIOD,
];
}
}

View file

@ -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;
}
}

View file

@ -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\Renderer\Block;
use League\CommonMark\Extension\CommonMark\Node\Block\ThematicBreak;
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 ThematicBreakRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param ThematicBreak $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
ThematicBreak::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('hr', $attrs, '', true);
}
public function getXmlTagName(Node $node): string
{
return 'thematic_break';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -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>
*
* 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\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Code;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\Xml;
use League\CommonMark\Xml\XmlNodeRendererInterface;
final class CodeRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Code $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Code::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('code', $attrs, Xml::escape($node->getLiteral()));
}
public function getXmlTagName(Node $node): string
{
return 'code';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -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\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Emphasis;
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 EmphasisRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Emphasis $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Emphasis::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('em', $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'emph';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -0,0 +1,66 @@
<?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\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\HtmlInline;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlFilter;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class HtmlInlineRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param HtmlInline $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): string
{
HtmlInline::assertInstanceOf($node);
$htmlInput = $this->config->get('html_input');
return HtmlFilter::filter($node->getLiteral(), $htmlInput);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'html_inline';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

View file

@ -0,0 +1,107 @@
<?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\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Image;
use League\CommonMark\Node\Inline\Newline;
use League\CommonMark\Node\Node;
use League\CommonMark\Node\NodeIterator;
use League\CommonMark\Node\StringContainerInterface;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\RegexHelper;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class ImageRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param Image $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Image::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links');
if ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl())) {
$attrs['src'] = '';
} else {
$attrs['src'] = $node->getUrl();
}
$attrs['alt'] = $this->getAltText($node);
if (($title = $node->getTitle()) !== null) {
$attrs['title'] = $title;
}
return new HtmlElement('img', $attrs, '', true);
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'image';
}
/**
* @param Image $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Image::assertInstanceOf($node);
return [
'destination' => $node->getUrl(),
'title' => $node->getTitle() ?? '',
];
}
private function getAltText(Image $node): string
{
$altText = '';
foreach ((new NodeIterator($node)) as $n) {
if ($n instanceof StringContainerInterface) {
$altText .= $n->getLiteral();
} elseif ($n instanceof Newline) {
$altText .= "\n";
}
}
return $altText;
}
}

View file

@ -0,0 +1,89 @@
<?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\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Util\RegexHelper;
use League\CommonMark\Xml\XmlNodeRendererInterface;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
final class LinkRenderer implements NodeRendererInterface, XmlNodeRendererInterface, ConfigurationAwareInterface
{
/** @psalm-readonly-allow-private-mutation */
private ConfigurationInterface $config;
/**
* @param Link $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Link::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
$forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links');
if (! ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl()))) {
$attrs['href'] = $node->getUrl();
}
if (($title = $node->getTitle()) !== null) {
$attrs['title'] = $title;
}
if (isset($attrs['target']) && $attrs['target'] === '_blank' && ! isset($attrs['rel'])) {
$attrs['rel'] = 'noopener noreferrer';
}
return new HtmlElement('a', $attrs, $childRenderer->renderNodes($node->children()));
}
public function setConfiguration(ConfigurationInterface $configuration): void
{
$this->config = $configuration;
}
public function getXmlTagName(Node $node): string
{
return 'link';
}
/**
* @param Link $node
*
* @return array<string, scalar>
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function getXmlAttributes(Node $node): array
{
Link::assertInstanceOf($node);
return [
'destination' => $node->getUrl(),
'title' => $node->getTitle() ?? '',
];
}
}

View file

@ -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\Renderer\Inline;
use League\CommonMark\Extension\CommonMark\Node\Inline\Strong;
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 StrongRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Strong $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Strong::assertInstanceOf($node);
$attrs = $node->data->get('attributes');
return new HtmlElement('strong', $attrs, $childRenderer->renderNodes($node->children()));
}
public function getXmlTagName(Node $node): string
{
return 'strong';
}
/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}

Some files were not shown because too many files have changed in this diff Show more