diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index e797ee11b7..d37ba2801f 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -28,7 +28,7 @@ expectedArguments(\League\CommonMark\Inline\Element\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types')); expectedReturnValues(\League\CommonMark\Inline\Element\Newline::getType(), argumentsSet('league_commonmark_newline_types')); - registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'html_input', 'allow_unsafe_links', 'max_nesting_level'); + registerArgumentsSet('league_commonmark_options', 'renderer', 'enable_em', 'enable_strong', 'use_asterisk', 'use_underscore', 'unordered_list_markers', 'html_input', 'allow_unsafe_links', 'max_nesting_level'); expectedArguments(\League\CommonMark\EnvironmentInterface::getConfig(), 0, argumentsSet('league_commonmark_options')); expectedArguments(\League\CommonMark\Util\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options')); expectedArguments(\League\CommonMark\Util\ConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options')); diff --git a/docs/1.0/configuration.md b/docs/1.0/configuration.md index 4d68429fe8..ad5d2d959a 100644 --- a/docs/1.0/configuration.md +++ b/docs/1.0/configuration.md @@ -24,6 +24,7 @@ $converter = new CommonMarkConverter([ 'enable_strong' => true, 'use_asterisk' => true, 'use_underscore' => true, + 'unordered_list_markers' => ['-', '*', '+'], 'html_input' => 'escape', 'allow_unsafe_links' => false, 'max_nesting_level' => INF, @@ -40,6 +41,7 @@ Here's a list of currently-supported options: * `enable_strong` - Disable `` parsing by setting to `false`; enable with `true` (default: `true`) * `use_asterisk` - Disable parsing of `*` for emphasis by setting to `false`; enable with `true` (default: `true`) * `use_underscore` - Disable parsing of `_` for emphasis by setting to `false`; enable with `true` (default: `true`) +* `unordered_list_markers` - Array of characters that can be used to indicated a bulleted list (default: `["-", "*", "+"]`) * `html_input` - How to handle HTML input. Set this option to one of the following strings: - `strip` - Strip all HTML (equivalent to `'safe' => true`) - `allow` - Allow all HTML input as-is (default value; equivalent to `'safe' => false) diff --git a/src/Block/Parser/ListParser.php b/src/Block/Parser/ListParser.php index 0ca69ab9fa..a438195587 100644 --- a/src/Block/Parser/ListParser.php +++ b/src/Block/Parser/ListParser.php @@ -20,10 +20,26 @@ use League\CommonMark\Block\Element\Paragraph; use League\CommonMark\ContextInterface; use League\CommonMark\Cursor; +use League\CommonMark\Util\ConfigurationAwareInterface; +use League\CommonMark\Util\ConfigurationInterface; use League\CommonMark\Util\RegexHelper; -final class ListParser implements BlockParserInterface +final class ListParser implements BlockParserInterface, ConfigurationAwareInterface { + /** @var ConfigurationInterface|null */ + private $config; + + /** @var string|null */ + private $listMarkerRegex; + + /** + * {@inheritdoc} + */ + public function setConfiguration(ConfigurationInterface $configuration) + { + $this->config = $configuration; + } + /** * @param ContextInterface $context * @param Cursor $cursor @@ -45,7 +61,7 @@ public function parse(ContextInterface $context, Cursor $cursor): bool $tmpCursor->advanceToNextNonSpaceOrTab(); $rest = $tmpCursor->getRemainder(); - if (\preg_match('/^[*+-]/', $rest) === 1) { + if (\preg_match($this->listMarkerRegex ?? $this->generateListMarkerRegex(), $rest) === 1) { $data = new ListData(); $data->markerOffset = $indent; $data->type = ListBlock::TYPE_UNORDERED; @@ -121,4 +137,20 @@ private function calculateListMarkerPadding(Cursor $cursor, int $markerLength): return $markerLength + $spacesAfterMarker; } + + private function generateListMarkerRegex(): string + { + // No configuration given - use the defaults + if ($this->config === null) { + return $this->listMarkerRegex = '/^[*+-]/'; + } + + $markers = $this->config->get('unordered_list_markers', ['*', '+', '-']); + + if (!\is_array($markers)) { + throw new \RuntimeException('Invalid configuration option "unordered_list_markers": value must be an array of strings'); + } + + return $this->listMarkerRegex = '/^[' . \preg_quote(\implode('', $markers), '/') . ']/'; + } } diff --git a/tests/unit/Block/Parser/ListParserTest.php b/tests/unit/Block/Parser/ListParserTest.php new file mode 100644 index 0000000000..c324b8ccb4 --- /dev/null +++ b/tests/unit/Block/Parser/ListParserTest.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace League\CommonMark\Tests\Unit\Block\Parser; + +use League\CommonMark\Block\Element\Document; +use League\CommonMark\Block\Element\ListBlock; +use League\CommonMark\Block\Element\ListItem; +use League\CommonMark\Block\Parser\ListParser; +use League\CommonMark\Context; +use League\CommonMark\Cursor; +use League\CommonMark\Environment; +use League\CommonMark\Util\Configuration; +use PHPUnit\Framework\TestCase; + +final class ListParserTest extends TestCase +{ + public function testOrderedListStartingAtOne() + { + $input = '1. Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_ORDERED, $container->getListData()->type); + $this->assertSame(1, $container->getListData()->start); + } + + public function testOrderedListStartingAtTwo() + { + $input = '2. Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_ORDERED, $container->getListData()->type); + $this->assertSame(2, $container->getListData()->start); + } + + public function testUnorderedListWithDashMarker() + { + $input = '- Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_UNORDERED, $container->getListData()->type); + $this->assertSame('-', $container->getListData()->bulletChar); + } + + public function testUnorderedListWithAsteriskMarker() + { + $input = '* Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_UNORDERED, $container->getListData()->type); + $this->assertSame('*', $container->getListData()->bulletChar); + } + + public function testUnorderedListWithPlusMarker() + { + $input = '+ Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_UNORDERED, $container->getListData()->type); + $this->assertSame('+', $container->getListData()->bulletChar); + } + + public function testUnorderedListWithCustomMarker() + { + $input = '^ Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $parser->setConfiguration(new Configuration(['unordered_list_markers' => ['^']])); + $this->assertTrue($parser->parse($context, $cursor)); + + $container = $context->getContainer(); + + $this->assertTrue($container instanceof ListItem); + /** @var ListItem $container */ + $this->assertSame(ListBlock::TYPE_UNORDERED, $container->getListData()->type); + $this->assertSame('^', $container->getListData()->bulletChar); + } + + public function testUnorderedListWithDisabledMarker() + { + $input = '+ Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $parser->setConfiguration(new Configuration(['unordered_list_markers' => ['-', '*']])); + $this->assertFalse($parser->parse($context, $cursor)); + } + + public function testInvalidListMarkerConfiguration() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid configuration option "unordered_list_markers": value must be an array of strings'); + $input = '+ Foo'; + + $context = new Context(new Document(), new Environment()); + $context->setNextLine($input); + $cursor = new Cursor($input); + + $parser = new ListParser(); + $parser->setConfiguration(new Configuration(['unordered_list_markers' => '-'])); + + $parser->parse($context, $cursor); + } +}