EnvironmentTest.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the league/commonmark package.
  5. *
  6. * (c) Colin O'Dell <colinodell@gmail.com>
  7. *
  8. * Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
  9. * - (c) John MacFarlane
  10. *
  11. * For the full copyright and license information, please view the LICENSE
  12. * file that was distributed with this source code.
  13. */
  14. namespace League\CommonMark\Tests\Unit\Environment;
  15. use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
  16. use League\CommonMark\Environment\Environment;
  17. use League\CommonMark\Environment\EnvironmentBuilderInterface;
  18. use League\CommonMark\Event\AbstractEvent;
  19. use League\CommonMark\Event\DocumentParsedEvent;
  20. use League\CommonMark\Exception\AlreadyInitializedException;
  21. use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
  22. use League\CommonMark\Extension\ConfigurableExtensionInterface;
  23. use League\CommonMark\Extension\ExtensionInterface;
  24. use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
  25. use League\CommonMark\Node\Block\Document;
  26. use League\CommonMark\Normalizer\TextNormalizerInterface;
  27. use League\CommonMark\Parser\Block\BlockStartParserInterface;
  28. use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser;
  29. use League\CommonMark\Parser\Inline\InlineParserInterface;
  30. use League\CommonMark\Renderer\NodeRendererInterface;
  31. use League\CommonMark\Tests\Unit\Event\FakeEvent;
  32. use League\CommonMark\Tests\Unit\Event\FakeEventListener;
  33. use League\CommonMark\Tests\Unit\Event\FakeEventListenerInvokable;
  34. use League\CommonMark\Tests\Unit\Event\FakeEventParent;
  35. use League\CommonMark\Util\ArrayCollection;
  36. use League\CommonMark\Util\HtmlFilter;
  37. use League\Config\ConfigurationBuilderInterface;
  38. use League\Config\ConfigurationInterface;
  39. use League\Config\MutableConfigurationInterface;
  40. use Nette\Schema\Expect;
  41. use Nette\Schema\Schema;
  42. use PHPUnit\Framework\TestCase;
  43. use Psr\EventDispatcher\EventDispatcherInterface;
  44. final class EnvironmentTest extends TestCase
  45. {
  46. public function testAddGetExtensions(): void
  47. {
  48. $environment = new Environment();
  49. $this->assertCount(0, $environment->getExtensions());
  50. $firstExtension = $this->createMock(ExtensionInterface::class);
  51. $firstExtension->expects($this->once())
  52. ->method('register')
  53. ->with($environment);
  54. $environment->addExtension($firstExtension);
  55. $extensions = $environment->getExtensions();
  56. $this->assertCount(1, $extensions);
  57. $this->assertEquals($firstExtension, $extensions[0]);
  58. $secondExtension = $this->createMock(ExtensionInterface::class);
  59. $secondExtension->expects($this->once())
  60. ->method('register')
  61. ->with($environment);
  62. $environment->addExtension($secondExtension);
  63. $extensions = $environment->getExtensions();
  64. $this->assertCount(2, $extensions);
  65. $this->assertEquals($firstExtension, $extensions[0]);
  66. $this->assertEquals($secondExtension, $extensions[1]);
  67. // Trigger initialization
  68. $environment->getBlockStartParsers();
  69. }
  70. public function testConstructor(): void
  71. {
  72. $config = ['max_nesting_level' => 42];
  73. $environment = new Environment($config);
  74. $this->assertSame(42, $environment->getConfiguration()->get('max_nesting_level'));
  75. }
  76. public function testGetConfiguration(): void
  77. {
  78. $config = ['max_nesting_level' => 3];
  79. $environment = new Environment($config);
  80. $configuration = $environment->getConfiguration();
  81. $this->assertInstanceOf(ConfigurationInterface::class, $configuration);
  82. $this->assertNotInstanceOf(MutableConfigurationInterface::class, $configuration);
  83. $this->assertSame(3, $configuration->get('max_nesting_level'));
  84. }
  85. public function testMergeConfig(): void
  86. {
  87. $environment = $this->createEnvironmentWithSchema([
  88. 'foo' => Expect::string(),
  89. 'test' => Expect::string(),
  90. ]);
  91. $environment->mergeConfig(['foo' => 'foo']);
  92. $this->assertEquals('foo', $environment->getConfiguration()->get('foo'));
  93. $this->assertNull($environment->getConfiguration()->get('test'));
  94. $environment->mergeConfig(['test' => '123', 'foo' => 'bar']);
  95. $this->assertEquals('bar', $environment->getConfiguration()->get('foo'));
  96. $this->assertEquals('123', $environment->getConfiguration()->get('test'));
  97. $environment->mergeConfig(['test' => '456']);
  98. $this->assertEquals('bar', $environment->getConfiguration()->get('foo'));
  99. $this->assertEquals('456', $environment->getConfiguration()->get('test'));
  100. }
  101. public function testMergeConfigAfterInit(): void
  102. {
  103. $this->expectException(AlreadyInitializedException::class);
  104. $environment = new Environment();
  105. // This triggers the initialization
  106. $environment->getBlockStartParsers();
  107. $environment->mergeConfig(['foo' => 'bar']);
  108. }
  109. public function testAddBlockStartParserAndGetter(): void
  110. {
  111. $environment = new Environment();
  112. $parser = $this->createMock(BlockStartParserInterface::class);
  113. $environment->addBlockStartParser($parser);
  114. $this->assertContains($parser, $environment->getBlockStartParsers());
  115. }
  116. public function testAddBlockStartParserFailsAfterInitialization(): void
  117. {
  118. $this->expectException(AlreadyInitializedException::class);
  119. $environment = new Environment();
  120. // This triggers the initialization
  121. $environment->getBlockStartParsers();
  122. $parser = $this->createMock(BlockStartParserInterface::class);
  123. $environment->addBlockStartParser($parser);
  124. }
  125. public function testAddRenderer(): void
  126. {
  127. $environment = new Environment();
  128. $renderer = $this->createMock(NodeRendererInterface::class);
  129. $environment->addRenderer('MyClass', $renderer);
  130. $this->assertContains($renderer, $environment->getRenderersForClass('MyClass'));
  131. }
  132. public function testAddRendererFailsAfterInitialization(): void
  133. {
  134. $this->expectException(AlreadyInitializedException::class);
  135. $environment = new Environment();
  136. // This triggers the initialization
  137. $environment->getRenderersForClass('MyClass');
  138. $renderer = $this->createMock(NodeRendererInterface::class);
  139. $environment->addRenderer('MyClass', $renderer);
  140. }
  141. public function testAddInlineParserFailsAfterInitialization(): void
  142. {
  143. $this->expectException(AlreadyInitializedException::class);
  144. $environment = new Environment();
  145. // This triggers the initialization
  146. $environment->getInlineParsers();
  147. $parser = $this->createMock(InlineParserInterface::class);
  148. $environment->addInlineParser($parser);
  149. }
  150. public function testAddDelimiterProcessor(): void
  151. {
  152. $environment = new Environment();
  153. $processor = $this->createMock(DelimiterProcessorInterface::class);
  154. $processor->method('getOpeningCharacter')->willReturn('*');
  155. $environment->addDelimiterProcessor($processor);
  156. $this->assertSame($processor, $environment->getDelimiterProcessors()->getDelimiterProcessor('*'));
  157. }
  158. public function testAddDelimiterProcessorFailsAfterInitialization(): void
  159. {
  160. $this->expectException(AlreadyInitializedException::class);
  161. $environment = new Environment();
  162. // This triggers the initialization
  163. $environment->getDelimiterProcessors();
  164. $processor = $this->createMock(DelimiterProcessorInterface::class);
  165. $environment->addDelimiterProcessor($processor);
  166. }
  167. public function testGetRendererForUnknownClass(): void
  168. {
  169. $environment = new Environment();
  170. $mockRenderer = $this->createMock(NodeRendererInterface::class);
  171. $environment->addRenderer(FakeBlock3::class, $mockRenderer);
  172. $this->assertEmpty($environment->getRenderersForClass(FakeBlock1::class));
  173. }
  174. public function testGetRendererForSubClass(): void
  175. {
  176. $environment = new Environment();
  177. $mockRenderer = $this->createMock(NodeRendererInterface::class);
  178. $environment->addRenderer(FakeBlock1::class, $mockRenderer);
  179. // Ensure the parent renderer is returned
  180. $this->assertFirstResult($mockRenderer, $environment->getRenderersForClass(FakeBlock3::class));
  181. // Check again to ensure any cached result is also the same
  182. $this->assertFirstResult($mockRenderer, $environment->getRenderersForClass(FakeBlock3::class));
  183. }
  184. public function testAddExtensionAndGetter(): void
  185. {
  186. $environment = new Environment();
  187. $extension = $this->createMock(ExtensionInterface::class);
  188. $environment->addExtension($extension);
  189. $this->assertContains($extension, $environment->getExtensions());
  190. }
  191. public function testAddExtensionFailsAfterInitialization(): void
  192. {
  193. $this->expectException(AlreadyInitializedException::class);
  194. $environment = new Environment();
  195. // This triggers the initialization
  196. $environment->getRenderersForClass('MyClass');
  197. $extension = $this->createMock(ExtensionInterface::class);
  198. $environment->addExtension($extension);
  199. }
  200. public function testInjectableBlockStartParsersGetInjected(): void
  201. {
  202. $environment = new Environment();
  203. $parser = new FakeInjectableBlockStartParser();
  204. $environment->addBlockStartParser($parser);
  205. // Trigger initialization
  206. $environment->getBlockStartParsers();
  207. $this->assertTrue($parser->bothWereInjected());
  208. }
  209. public function testInjectableRenderersGetInjected(): void
  210. {
  211. $environment = new Environment();
  212. $renderer = new FakeInjectableRenderer();
  213. $environment->addRenderer('', $renderer);
  214. // Trigger initialization
  215. $environment->getBlockStartParsers();
  216. $this->assertTrue($renderer->bothWereInjected());
  217. }
  218. public function testInjectableInlineParsersGetInjected(): void
  219. {
  220. $environment = new Environment();
  221. $parser = new FakeInjectableInlineParser();
  222. $environment->addInlineParser($parser);
  223. // Trigger initialization
  224. $environment->getBlockStartParsers();
  225. $this->assertTrue($parser->bothWereInjected());
  226. }
  227. public function testInjectableDelimiterProcessorsGetInjected(): void
  228. {
  229. $environment = new Environment();
  230. $processor = new FakeInjectableDelimiterProcessor();
  231. $environment->addDelimiterProcessor($processor);
  232. // Trigger initialization
  233. $environment->getBlockStartParsers();
  234. $this->assertTrue($processor->bothWereInjected());
  235. }
  236. public function testInjectableEventListenersGetInjected(): void
  237. {
  238. $environment = new Environment();
  239. // phpcs:ignore Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore
  240. $listener1 = new FakeEventListener(static function (): void { });
  241. // phpcs:ignore Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore
  242. $listener2 = new FakeEventListenerInvokable(static function (): void { });
  243. $environment->addEventListener('', [$listener1, 'doStuff']);
  244. $environment->addEventListener('', $listener2);
  245. // Trigger initialization
  246. $environment->getBlockStartParsers();
  247. $this->assertSame($environment, $listener1->getEnvironment());
  248. $this->assertSame($environment, $listener2->getEnvironment());
  249. $this->assertNotNull($listener1->getConfiguration());
  250. $this->assertNotNull($listener2->getConfiguration());
  251. }
  252. public function testSkipLinesParserIncludedByDefault(): void
  253. {
  254. $environment = new Environment();
  255. $parsers = \iterator_to_array($environment->getBlockStartParsers());
  256. $this->assertCount(1, $parsers);
  257. $this->assertInstanceOf(SkipLinesStartingWithLettersParser::class, $parsers[0]);
  258. }
  259. public function testBlockParserPrioritization(): void
  260. {
  261. $environment = new Environment();
  262. $parser1 = $this->createMock(BlockStartParserInterface::class);
  263. $parser2 = $this->createMock(BlockStartParserInterface::class);
  264. $parser3 = $this->createMock(BlockStartParserInterface::class);
  265. $environment->addBlockStartParser($parser1);
  266. $environment->addBlockStartParser($parser2, 500);
  267. $environment->addBlockStartParser($parser3);
  268. $parsers = \iterator_to_array($environment->getBlockStartParsers());
  269. $this->assertSame($parser2, $parsers[0]);
  270. $this->assertInstanceOf(SkipLinesStartingWithLettersParser::class, $parsers[1]);
  271. $this->assertSame($parser1, $parsers[2]);
  272. $this->assertSame($parser3, $parsers[3]);
  273. }
  274. public function testGetInlineParsersWithPrioritization(): void
  275. {
  276. $environment = new Environment();
  277. $parser1 = $this->createMock(InlineParserInterface::class);
  278. $parser2 = $this->createMock(InlineParserInterface::class);
  279. $parser3 = $this->createMock(InlineParserInterface::class);
  280. $environment->addInlineParser($parser1);
  281. $environment->addInlineParser($parser2, 50);
  282. $environment->addInlineParser($parser3);
  283. $parsers = \iterator_to_array($environment->getInlineParsers());
  284. $this->assertSame($parser2, $parsers[0]);
  285. $this->assertSame($parser1, $parsers[1]);
  286. $this->assertSame($parser3, $parsers[2]);
  287. }
  288. public function testRendererPrioritization(): void
  289. {
  290. $environment = new Environment();
  291. $renderer1 = $this->createMock(NodeRendererInterface::class);
  292. $renderer2 = $this->createMock(NodeRendererInterface::class);
  293. $renderer3 = $this->createMock(NodeRendererInterface::class);
  294. $environment->addRenderer('foo', $renderer1);
  295. $environment->addRenderer('foo', $renderer2, 50);
  296. $environment->addRenderer('foo', $renderer3);
  297. $parsers = \iterator_to_array($environment->getRenderersForClass('foo'));
  298. $this->assertSame($renderer2, $parsers[0]);
  299. $this->assertSame($renderer1, $parsers[1]);
  300. $this->assertSame($renderer3, $parsers[2]);
  301. }
  302. public function testEventDispatching(): void
  303. {
  304. $environment = new Environment();
  305. $event = new FakeEvent();
  306. $actualOrder = [];
  307. $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder): void {
  308. $this->assertSame($event, $e);
  309. $actualOrder[] = 'a';
  310. });
  311. // Listeners on parent classes should also be called
  312. $environment->addEventListener(FakeEventParent::class, function (FakeEvent $e) use ($event, &$actualOrder): void {
  313. $this->assertSame($event, $e);
  314. $actualOrder[] = 'b';
  315. $e->stopPropagation();
  316. });
  317. $environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder): void {
  318. $this->assertSame($event, $e);
  319. $actualOrder[] = 'c';
  320. }, 10);
  321. $environment->addEventListener(FakeEvent::class, function (FakeEvent $e): void {
  322. $this->fail('Propogation should have been stopped before here');
  323. });
  324. $environment->dispatch($event);
  325. $this->assertCount(3, $actualOrder);
  326. $this->assertEquals('c', $actualOrder[0]);
  327. $this->assertEquals('a', $actualOrder[1]);
  328. $this->assertEquals('b', $actualOrder[2]);
  329. }
  330. public function testAddEventListenerFailsAfterInitialization(): void
  331. {
  332. $this->expectException(AlreadyInitializedException::class);
  333. $environment = new Environment();
  334. // Trigger initialization
  335. $environment->dispatch($this->createMock(AbstractEvent::class));
  336. $environment->addEventListener(AbstractEvent::class, static function (AbstractEvent $e): void {
  337. });
  338. }
  339. public function testDispatchDelegatesToProvidedDispatcher(): void
  340. {
  341. $dispatchersCalled = new ArrayCollection();
  342. $environment = new Environment();
  343. $environment->addEventListener(FakeEvent::class, static function (FakeEvent $event) use ($dispatchersCalled): void {
  344. $dispatchersCalled[] = 'THIS SHOULD NOT BE CALLED!';
  345. });
  346. $environment->setEventDispatcher(new class ($dispatchersCalled) implements EventDispatcherInterface {
  347. private ArrayCollection $dispatchersCalled;
  348. public function __construct(ArrayCollection $dispatchersCalled)
  349. {
  350. $this->dispatchersCalled = $dispatchersCalled;
  351. }
  352. public function dispatch(object $event): object
  353. {
  354. $this->dispatchersCalled[] = 'external';
  355. return $event;
  356. }
  357. });
  358. $environment->dispatch(new FakeEvent());
  359. $this->assertCount(1, $dispatchersCalled);
  360. $this->assertSame('external', $dispatchersCalled->first());
  361. }
  362. public function testGetDefaultSlugNormalizer(): void
  363. {
  364. $environment = new Environment();
  365. $normalizer = $environment->getSlugNormalizer();
  366. $this->assertSame('test', $normalizer->normalize('Test'));
  367. $this->assertSame('test-1', $normalizer->normalize('Test'));
  368. }
  369. public function testCustomSlugNormalizer(): void
  370. {
  371. $innerNormalizer = $this->createStub(TextNormalizerInterface::class);
  372. $innerNormalizer->method('normalize')->willReturn('foo');
  373. $environment = new Environment([
  374. 'slug_normalizer' => [
  375. 'instance' => $innerNormalizer,
  376. ],
  377. ]);
  378. $normalizer = $environment->getSlugNormalizer();
  379. $this->assertSame('foo', $normalizer->normalize('Foo'));
  380. $this->assertSame('foo-1', $normalizer->normalize('Foo'));
  381. }
  382. public function testUniqueSlugNormalizerDisabled(): void
  383. {
  384. $environment = new Environment([
  385. 'slug_normalizer' => [
  386. 'unique' => false,
  387. ],
  388. ]);
  389. $normalizer = $environment->getSlugNormalizer();
  390. $this->assertSame('foo', $normalizer->normalize('Foo'));
  391. $this->assertSame('foo', $normalizer->normalize('Foo'));
  392. $this->assertSame('foo', $normalizer->normalize('Foo'));
  393. }
  394. public function testUniqueSlugNormalizerPerDocument(): void
  395. {
  396. $environment = new Environment([
  397. 'slug_normalizer' => [
  398. 'unique' => 'document',
  399. ],
  400. ]);
  401. $normalizer = $environment->getSlugNormalizer();
  402. $this->assertSame('foo', $normalizer->normalize('Foo'));
  403. $this->assertSame('foo-1', $normalizer->normalize('Foo'));
  404. $this->assertSame('foo-2', $normalizer->normalize('Foo'));
  405. $environment->dispatch(new DocumentParsedEvent(new Document()));
  406. $this->assertSame('foo', $normalizer->normalize('Foo'));
  407. $this->assertSame('foo-1', $normalizer->normalize('Foo'));
  408. $this->assertSame('foo-2', $normalizer->normalize('Foo'));
  409. }
  410. public function testUniqueSlugNormalizerPerEnvironment(): void
  411. {
  412. $environment = new Environment([
  413. 'slug_normalizer' => [
  414. 'unique' => 'environment',
  415. ],
  416. ]);
  417. $normalizer = $environment->getSlugNormalizer();
  418. $this->assertSame('foo', $normalizer->normalize('Foo'));
  419. $this->assertSame('foo-1', $normalizer->normalize('Foo'));
  420. $this->assertSame('foo-2', $normalizer->normalize('Foo'));
  421. $environment->dispatch(new DocumentParsedEvent(new Document()));
  422. $this->assertSame('foo-3', $normalizer->normalize('Foo'));
  423. $this->assertSame('foo-4', $normalizer->normalize('Foo'));
  424. $this->assertSame('foo-5', $normalizer->normalize('Foo'));
  425. }
  426. /**
  427. * @param mixed $expected
  428. * @param iterable<mixed> $actual
  429. */
  430. private function assertFirstResult($expected, iterable $actual): void
  431. {
  432. foreach ($actual as $a) {
  433. $this->assertSame($expected, $a);
  434. return;
  435. }
  436. $this->assertSame($expected, null);
  437. }
  438. /**
  439. * @param array<string, Schema> $schemas
  440. */
  441. private function createEnvironmentWithSchema(array $schemas): Environment
  442. {
  443. $environment = new Environment();
  444. $environment->addExtension(new class ($schemas) implements ConfigurableExtensionInterface {
  445. /** @var array<string, Schema> */
  446. private array $schemas;
  447. /**
  448. * @param array<string, Schema> $schemas
  449. */
  450. public function __construct(array $schemas)
  451. {
  452. $this->schemas = $schemas;
  453. }
  454. public function configureSchema(ConfigurationBuilderInterface $builder): void
  455. {
  456. foreach ($this->schemas as $key => $schema) {
  457. $builder->addSchema($key, $schema);
  458. }
  459. }
  460. public function register(EnvironmentBuilderInterface $environment): void
  461. {
  462. }
  463. });
  464. return $environment;
  465. }
  466. public function testCreateCommonMarkEnvironment(): void
  467. {
  468. $environment = Environment::createCommonMarkEnvironment(['html_input' => HtmlFilter::ESCAPE]);
  469. $this->assertCount(1, $environment->getExtensions());
  470. $this->assertInstanceOf(CommonMarkCoreExtension::class, $environment->getExtensions()[0]);
  471. $this->assertSame(HtmlFilter::ESCAPE, $environment->getConfiguration()->get('html_input'));
  472. }
  473. public function testCreateGFMEnvironment(): void
  474. {
  475. $environment = Environment::createGFMEnvironment(['html_input' => HtmlFilter::ESCAPE]);
  476. $this->assertCount(2, $environment->getExtensions());
  477. $this->assertInstanceOf(CommonMarkCoreExtension::class, $environment->getExtensions()[0]);
  478. $this->assertInstanceOf(GithubFlavoredMarkdownExtension::class, $environment->getExtensions()[1]);
  479. $this->assertSame(HtmlFilter::ESCAPE, $environment->getConfiguration()->get('html_input'));
  480. }
  481. }