diff --git a/src/Block/Element/Color.php b/src/Block/Element/Color.php index d89485f..d00072f 100644 --- a/src/Block/Element/Color.php +++ b/src/Block/Element/Color.php @@ -2,11 +2,11 @@ namespace JohnnyHuy\Laravel\Block\Element; -use League\CommonMark\Block\Element\AbstractBlock; -use League\CommonMark\Block\Element\InlineContainerInterface; -use League\CommonMark\ContextInterface; use League\CommonMark\Cursor; use League\CommonMark\Util\RegexHelper; +use League\CommonMark\ContextInterface; +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Block\Element\InlineContainerInterface; class Color extends AbstractBlock implements InlineContainerInterface { /** diff --git a/src/Block/Element/TextAlignment.php b/src/Block/Element/TextAlignment.php index 3408d3d..88f3b70 100644 --- a/src/Block/Element/TextAlignment.php +++ b/src/Block/Element/TextAlignment.php @@ -2,11 +2,11 @@ namespace JohnnyHuy\Laravel\Block\Element; -use League\CommonMark\Block\Element\AbstractBlock; -use League\CommonMark\Block\Element\InlineContainerInterface; -use League\CommonMark\ContextInterface; use League\CommonMark\Cursor; use League\CommonMark\Util\RegexHelper; +use League\CommonMark\ContextInterface; +use League\CommonMark\Block\Element\AbstractBlock; +use League\CommonMark\Block\Element\InlineContainerInterface; class TextAlignment extends AbstractBlock implements InlineContainerInterface { /** diff --git a/src/Block/Parser/ColorParser.php b/src/Block/Parser/ColorParser.php index f546a21..cbb73dc 100644 --- a/src/Block/Parser/ColorParser.php +++ b/src/Block/Parser/ColorParser.php @@ -4,11 +4,10 @@ namespace JohnnyHuy\Laravel\Block\Parser; +use League\CommonMark\Cursor; +use League\CommonMark\ContextInterface; use JohnnyHuy\Laravel\Block\Element\Color; -use JohnnyHuy\Laravel\Block\Element\TextAlignment; use League\CommonMark\Block\Parser\AbstractBlockParser; -use League\CommonMark\ContextInterface; -use League\CommonMark\Cursor; /** * Text alignment parser class. diff --git a/src/Block/Parser/TextAlignmentParser.php b/src/Block/Parser/TextAlignmentParser.php index 6678c20..e114333 100755 --- a/src/Block/Parser/TextAlignmentParser.php +++ b/src/Block/Parser/TextAlignmentParser.php @@ -4,10 +4,10 @@ namespace JohnnyHuy\Laravel\Block\Parser; +use League\CommonMark\Cursor; +use League\CommonMark\ContextInterface; use JohnnyHuy\Laravel\Block\Element\TextAlignment; use League\CommonMark\Block\Parser\AbstractBlockParser; -use League\CommonMark\ContextInterface; -use League\CommonMark\Cursor; /** * Text alignment parser class. diff --git a/src/Block/Renderer/ColorRenderer.php b/src/Block/Renderer/ColorRenderer.php index a8296dc..afd56ba 100644 --- a/src/Block/Renderer/ColorRenderer.php +++ b/src/Block/Renderer/ColorRenderer.php @@ -2,14 +2,13 @@ namespace JohnnyHuy\Laravel\Block\Renderer; +use League\CommonMark\HtmlElement; +use League\CommonMark\Util\Configuration; use JohnnyHuy\Laravel\Block\Element\Color; -use JohnnyHuy\Laravel\Block\Element\TextAlignment; -use League\CommonMark\Block\Element\AbstractBlock; -use League\CommonMark\Block\Renderer\BlockRendererInterface; use League\CommonMark\ElementRendererInterface; -use League\CommonMark\HtmlElement; +use League\CommonMark\Block\Element\AbstractBlock; use League\CommonMark\Inline\Element\AbstractInline; -use League\CommonMark\Util\Configuration; +use League\CommonMark\Block\Renderer\BlockRendererInterface; class ColorRenderer implements BlockRendererInterface { diff --git a/src/Block/Renderer/TextAlignmentRenderer.php b/src/Block/Renderer/TextAlignmentRenderer.php index 1bc59b6..a146899 100644 --- a/src/Block/Renderer/TextAlignmentRenderer.php +++ b/src/Block/Renderer/TextAlignmentRenderer.php @@ -2,13 +2,13 @@ namespace JohnnyHuy\Laravel\Block\Renderer; +use League\CommonMark\HtmlElement; +use League\CommonMark\Util\Configuration; +use League\CommonMark\ElementRendererInterface; use JohnnyHuy\Laravel\Block\Element\TextAlignment; use League\CommonMark\Block\Element\AbstractBlock; -use League\CommonMark\Block\Renderer\BlockRendererInterface; -use League\CommonMark\ElementRendererInterface; -use League\CommonMark\HtmlElement; use League\CommonMark\Inline\Element\AbstractInline; -use League\CommonMark\Util\Configuration; +use League\CommonMark\Block\Renderer\BlockRendererInterface; class TextAlignmentRenderer implements BlockRendererInterface { diff --git a/src/Inline/Element/Codepen.php b/src/Inline/Element/Codepen.php new file mode 100644 index 0000000..e5a1667 --- /dev/null +++ b/src/Inline/Element/Codepen.php @@ -0,0 +1,7 @@ +getCursor(); + $savedState = $cursor->saveState(); + + $cursor->advance(); + + //check that the given user input is a valid codepen url + //and the required `codepen:` prefix exists + $regex = '/^(?:codepen)\s(https:\/\/codepen\.io\/([^\/]+\/)?([a-zA-Z0-9]+)\/pen\/([a-zA-Z0-9]+)?)/'; + $validate = $cursor->match($regex); + + //the computer says no + if (!$validate) { + $cursor->restoreState($savedState); + + return false; + } + + $matches = []; + preg_match($regex, $validate, $matches); + + //return the given codepen url to the renderer class + $inlineContext->getContainer()->appendChild(new Codepen($matches[1])); + + return true; + } + + /** + * @return string[] + */ + public function getCharacters() + { + return [':']; + } +} diff --git a/src/Inline/Parser/GistParser.php b/src/Inline/Parser/GistParser.php new file mode 100755 index 0000000..8da67bd --- /dev/null +++ b/src/Inline/Parser/GistParser.php @@ -0,0 +1,52 @@ +getCursor(); + $savedState = $cursor->saveState(); + + $cursor->advance(); + + //check that the given user input is a valid gist url + //and the required `gist:` prefix exists + $regex = '/^(?:gist)\s(https:\/\/gist.github.com\/([^\/]+\/)?([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)?)/'; + $validate = $cursor->match($regex); + + //the computer says no + if (!$validate) { + $cursor->restoreState($savedState); + + return false; + } + + $matches = []; + preg_match($regex, $validate, $matches); + + //return the given gist url to the renderer class + $inlineContext->getContainer()->appendChild(new Gist($matches[1])); + + return true; + } + + /** + * @return string[] + */ + public function getCharacters() + { + return [':']; + } +} diff --git a/src/Inline/Parser/SoundCloudParser.php b/src/Inline/Parser/SoundCloudParser.php index 0970ce9..c3e3bfb 100755 --- a/src/Inline/Parser/SoundCloudParser.php +++ b/src/Inline/Parser/SoundCloudParser.php @@ -4,10 +4,9 @@ namespace JohnnyHuy\Laravel\Inline\Parser; +use League\CommonMark\InlineParserContext; use JohnnyHuy\Laravel\Inline\Element\SoundCloud; -use JohnnyHuy\Laravel\Inline\Element\YouTube; use League\CommonMark\Inline\Parser\AbstractInlineParser; -use League\CommonMark\InlineParserContext; class SoundCloudParser extends AbstractInlineParser { @@ -22,9 +21,12 @@ public function parse(InlineParserContext $inlineContext) $cursor->advance(); + //check that the given user input is a valid soundcloud url + //and the required `soundcloud:` or `:sc` prefix exists $regex = '/^(?:soundcloud|sc)\s((?:https?\:\/\/)?(?:www\.)?(?:soundcloud\.com\/)[^&#\s\?]+\/[^&#\s\?]+)/'; $validate = $cursor->match($regex); + //the computer says no if (!$validate) { $cursor->restoreState($savedState); diff --git a/src/Inline/Parser/YouTubeParser.php b/src/Inline/Parser/YouTubeParser.php index fb0d22e..9227d6f 100755 --- a/src/Inline/Parser/YouTubeParser.php +++ b/src/Inline/Parser/YouTubeParser.php @@ -4,9 +4,9 @@ namespace JohnnyHuy\Laravel\Inline\Parser; +use League\CommonMark\InlineParserContext; use JohnnyHuy\Laravel\Inline\Element\YouTube; use League\CommonMark\Inline\Parser\AbstractInlineParser; -use League\CommonMark\InlineParserContext; class YouTubeParser extends AbstractInlineParser { @@ -21,9 +21,12 @@ public function parse(InlineParserContext $inlineContext) $cursor->advance(); + //regex to ensure that we got a valid youtube url + //and the required `youtube:` prefix exists $regex = '/^(?:youtube)\s(?:https?\:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&#\s\?]+)(?:\?.[^\s]+)?/'; $validate = $cursor->match($regex); + //the computer says no if (!$validate) { $cursor->restoreState($savedState); @@ -34,6 +37,7 @@ public function parse(InlineParserContext $inlineContext) preg_match($regex, $validate, $matches); $videoId = $matches[1]; + //generates a valid youtube embed url with the parsed video id from the given url $inlineContext->getContainer()->appendChild(new YouTube("https://www.youtube.com/embed/$videoId")); return true; diff --git a/src/Inline/Renderer/CodepenRenderer.php b/src/Inline/Renderer/CodepenRenderer.php new file mode 100644 index 0000000..4b1ed85 --- /dev/null +++ b/src/Inline/Renderer/CodepenRenderer.php @@ -0,0 +1,60 @@ +getUrl()}&format=json"; + + $apiResponse = $this->getContent($apiUrl); + + //seems that the used codepen url is invalid + //or codepen is currently not available + if (is_null($apiResponse)) { + throw new \ErrorException('Codepen request returned null: ' . $apiUrl); + } + + //parse the oembed response + $embed = json_decode($apiResponse); + + //return the oembed html snippet with a div as wrapper element + return new HtmlElement('div', ['class' => 'codepen-container'], $embed->html); + } + + /** + * @param string $url + * @return string + */ + public function getContent(string $url): string + { + return file_get_contents($url); + } +} diff --git a/src/Inline/Renderer/GistRenderer.php b/src/Inline/Renderer/GistRenderer.php new file mode 100644 index 0000000..0468d9e --- /dev/null +++ b/src/Inline/Renderer/GistRenderer.php @@ -0,0 +1,51 @@ + $inline->getUrl().'.js' + ]); + + //add a div wrapper around the script element + return new HtmlElement('div', ['class' => 'gist-container'], $script); + } + + /** + * @param string $url + * @return string + */ + public function getContent(string $url) : string + { + return file_get_contents($url); + } +} diff --git a/src/Inline/Renderer/SoundCloudRenderer.php b/src/Inline/Renderer/SoundCloudRenderer.php index 7432bbf..352e41c 100644 --- a/src/Inline/Renderer/SoundCloudRenderer.php +++ b/src/Inline/Renderer/SoundCloudRenderer.php @@ -2,15 +2,14 @@ namespace JohnnyHuy\Laravel\Inline\Renderer; -use JohnnyHuy\Laravel\Inline\Element\SoundCloud; -use JohnnyHuy\Laravel\Inline\Element\YouTube; -use League\CommonMark\ElementRendererInterface; use League\CommonMark\HtmlElement; +use League\CommonMark\Util\Configuration; +use League\CommonMark\ElementRendererInterface; +use JohnnyHuy\Laravel\Inline\Element\SoundCloud; use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Util\ConfigurationAwareInterface; use League\CommonMark\Inline\Element\AbstractWebResource; use League\CommonMark\Inline\Renderer\InlineRendererInterface; -use League\CommonMark\Util\Configuration; -use League\CommonMark\Util\ConfigurationAwareInterface; class SoundCloudRenderer implements InlineRendererInterface { @@ -36,12 +35,16 @@ public function render(AbstractInline $inline, ElementRendererInterface $htmlRen $url = "https://soundcloud.com/oembed?&format=json&url={$inline->getUrl()}&maxheight=166"; $soundCloud = $this->getContent($url); + //seems that the used soundcloud url is invalid + //or soundcloud is currently not available if (is_null($soundCloud)) { throw new \ErrorException('SoundCloud request returned null: ' . $url); } + //parse the oembed response $soundCloud = json_decode($soundCloud); + //use the oembed html snippet as response return $soundCloud->html; } diff --git a/src/Inline/Renderer/YouTubeRenderer.php b/src/Inline/Renderer/YouTubeRenderer.php index a938db2..52cca42 100644 --- a/src/Inline/Renderer/YouTubeRenderer.php +++ b/src/Inline/Renderer/YouTubeRenderer.php @@ -2,14 +2,14 @@ namespace JohnnyHuy\Laravel\Inline\Renderer; +use League\CommonMark\HtmlElement; +use League\CommonMark\Util\Configuration; use JohnnyHuy\Laravel\Inline\Element\YouTube; use League\CommonMark\ElementRendererInterface; -use League\CommonMark\HtmlElement; use League\CommonMark\Inline\Element\AbstractInline; +use League\CommonMark\Util\ConfigurationAwareInterface; use League\CommonMark\Inline\Element\AbstractWebResource; use League\CommonMark\Inline\Renderer\InlineRendererInterface; -use League\CommonMark\Util\Configuration; -use League\CommonMark\Util\ConfigurationAwareInterface; class YouTubeRenderer implements InlineRendererInterface, ConfigurationAwareInterface { @@ -30,6 +30,7 @@ public function render(AbstractInline $inline, ElementRendererInterface $htmlRen throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline)); } + //create a new iframe with the given youtube url $iframe = new HtmlElement('iframe', [ 'width' => 640, 'height' => 390, @@ -38,6 +39,7 @@ public function render(AbstractInline $inline, ElementRendererInterface $htmlRen 'frameborder' => 0, ]); + //return the iframe with a span as wrapper element return new HtmlElement('span', ['class' => 'youtube-video'], $iframe); } diff --git a/src/UsefulCommonMarkExtension.php b/src/UsefulCommonMarkExtension.php index 159fb17..58ef9ff 100755 --- a/src/UsefulCommonMarkExtension.php +++ b/src/UsefulCommonMarkExtension.php @@ -6,17 +6,23 @@ use Illuminate\Container\Container; use JohnnyHuy\Laravel\Block\Element\Color; -use JohnnyHuy\Laravel\Block\Element\TextAlignment; use JohnnyHuy\Laravel\Block\Parser\ColorParser; -use JohnnyHuy\Laravel\Block\Parser\TextAlignmentParser; use JohnnyHuy\Laravel\Block\Renderer\ColorRenderer; +use JohnnyHuy\Laravel\Block\Element\TextAlignment; +use JohnnyHuy\Laravel\Block\Parser\TextAlignmentParser; use JohnnyHuy\Laravel\Block\Renderer\TextAlignmentRenderer; -use JohnnyHuy\Laravel\Inline\Element\SoundCloud; use JohnnyHuy\Laravel\Inline\Element\YouTube; -use JohnnyHuy\Laravel\Inline\Parser\SoundCloudParser; use JohnnyHuy\Laravel\Inline\Parser\YouTubeParser; -use JohnnyHuy\Laravel\Inline\Renderer\SoundCloudRenderer; use JohnnyHuy\Laravel\Inline\Renderer\YouTubeRenderer; +use JohnnyHuy\Laravel\Inline\Element\SoundCloud; +use JohnnyHuy\Laravel\Inline\Parser\SoundCloudParser; +use JohnnyHuy\Laravel\Inline\Renderer\SoundCloudRenderer; +use JohnnyHuy\Laravel\Inline\Element\Gist; +use JohnnyHuy\Laravel\Inline\Parser\GistParser; +use JohnnyHuy\Laravel\Inline\Renderer\GistRenderer; +use JohnnyHuy\Laravel\Inline\Element\Codepen; +use JohnnyHuy\Laravel\Inline\Parser\CodepenParser; +use JohnnyHuy\Laravel\Inline\Renderer\CodepenRenderer; use League\CommonMark\Extension\Extension; /** @@ -54,11 +60,15 @@ class UsefulCommonMarkExtension extends Extension public function __construct(Container $container) { $this->inlineParsers = [ + $container->make(GistParser::class), + $container->make(CodepenParser::class), $container->make(YouTubeParser::class), $container->make(SoundCloudParser::class), ]; $this->inlineRenderers = [ + Gist::class => $container->make(GistRenderer::class), + Codepen::class => $container->make(CodepenRenderer::class), YouTube::class => $container->make(YouTubeRenderer::class), SoundCloud::class => $container->make(SoundCloudRenderer::class), ]; diff --git a/src/UsefulCommonMarkExtensionServiceProvider.php b/src/UsefulCommonMarkExtensionServiceProvider.php index 5d79d44..7010b86 100755 --- a/src/UsefulCommonMarkExtensionServiceProvider.php +++ b/src/UsefulCommonMarkExtensionServiceProvider.php @@ -6,8 +6,11 @@ use Illuminate\Support\ServiceProvider; use JohnnyHuy\Laravel\Block\Parser\ColorParser; -use JohnnyHuy\Laravel\Inline\Parser\SoundCloudParser; +use JohnnyHuy\Laravel\Inline\Parser\GistParser; +use JohnnyHuy\Laravel\Inline\Parser\CodepenParser; use JohnnyHuy\Laravel\Inline\Parser\YouTubeParser; +use JohnnyHuy\Laravel\Inline\Parser\SoundCloudParser; +use JohnnyHuy\Laravel\Block\Parser\TextAlignmentParser; /** * This is the CommonMark useful extension service provider class. @@ -33,9 +36,11 @@ public function register() */ protected function registerParser() { + $this->app->singleton(GistParser::class, function () { return new GistParser(); }); + $this->app->singleton(ColorParser::class, function () { return new ColorParser(); }); + $this->app->singleton(CodepenParser::class, function () { return new CodepenParser(); }); $this->app->singleton(YouTubeParser::class, function () { return new YouTubeParser(); }); $this->app->singleton(SoundCloudParser::class, function () { return new SoundCloudParser(); }); - $this->app->singleton(ColorParser::class, function () { return new ColorParser(); }); } /** diff --git a/tests/Elements/CodepenTest.php b/tests/Elements/CodepenTest.php new file mode 100644 index 0000000..feaef34 --- /dev/null +++ b/tests/Elements/CodepenTest.php @@ -0,0 +1,58 @@ + + */ +class CodepenTest extends BaseTestCase +{ + public function successfulStrings() + { + $expected = '

'; + + return [ + [':codepen https://codepen.io/YusukeNakaya/pen/XyOaBj', $expected] + ]; + } + + public function failedStrings() + { + return [ + // gist keyword is not separated with a space + [':codepenhttps://codepen.io/YusukeNakaya/pen/XyOaBj', '

:codepenhttps://codepen.io/YusukeNakaya/pen/XyOaBj

'], + + // Didn't include the ':gist' keyword + ['https://codepen.io/YusukeNakaya/pen/XyOaBj', '

https://codepen.io/YusukeNakaya/pen/XyOaBj

'], + + // Invalid codepen URLs + [':codepen https//gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', '

:codepen https//gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e

'] + ]; + } + + /** + * @dataProvider successfulStrings + * @param $input + * @param $output + */ + public function testShouldRender($input, $output) + { + $this->assertSame("$output\n", $this->app->markdown->convertToHtml($input)); + } + + /** + * @dataProvider failedStrings + * @param $input + * @param $output + */ + public function testShouldNotRender($input, $output) + { + $this->assertSame("$output\n", $this->app->markdown->convertToHtml($input)); + } +} \ No newline at end of file diff --git a/tests/Elements/GistTest.php b/tests/Elements/GistTest.php new file mode 100644 index 0000000..8ae48ab --- /dev/null +++ b/tests/Elements/GistTest.php @@ -0,0 +1,59 @@ + + */ +class GistTest extends BaseTestCase +{ + public function successfulStrings() + { + $expected = '

'; + + return [ + [':gist https://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', $expected] + ]; + } + + public function failedStrings() + { + return [ + // gist keyword is not separated with a space + [':gisthttps://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', '

:gisthttps://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e

'], + + // Didn't include the ':gist' keyword + ['https://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', '

https://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e

'], + + // Invalid gist URLs + [':gist https//gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', '

:gist https//gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e

'], + [':gist http://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e', '

:gist http://gist.github.com/noxify/2b02fd0fb0ea18a4d9d764e31fe9af8e

'], + ]; + } + + /** + * @dataProvider successfulStrings + * @param $input + * @param $output + */ + public function testShouldRender($input, $output) + { + $this->assertSame("$output\n", $this->app->markdown->convertToHtml($input)); + } + + /** + * @dataProvider failedStrings + * @param $input + * @param $output + */ + public function testShouldNotRender($input, $output) + { + $this->assertSame("$output\n", $this->app->markdown->convertToHtml($input)); + } +} \ No newline at end of file diff --git a/tests/Laravel/ServiceProviderTest.php b/tests/Laravel/ServiceProviderTest.php index 7e052fb..45a24c5 100644 --- a/tests/Laravel/ServiceProviderTest.php +++ b/tests/Laravel/ServiceProviderTest.php @@ -9,6 +9,8 @@ use JohnnyHuy\Laravel\Block\Parser\ColorParser; use JohnnyHuy\Laravel\Inline\Parser\YouTubeParser; use JohnnyHuy\Laravel\Markdown\Tests\BaseTestCase; +use JohnnyHuy\Laravel\Inline\Parser\GistParser; +use JohnnyHuy\Laravel\Inline\Parser\CodepenParser; use JohnnyHuy\Laravel\UsefulCommonMarkExtension; use League\CommonMark\Environment; @@ -25,6 +27,8 @@ class ServiceProviderTest extends BaseTestCase public function testMarkdownParserIsInjectable() { $this->assertIsInjectable(YouTubeParser::class); + $this->assertIsInjectable(GistParser::class); + $this->assertIsInjectable(CodepenParser::class); } public function testMarkdownExtensionIsInjectable() @@ -43,6 +47,8 @@ public function testInjectParsers() $environment = $this->app->get(Environment::class); $this->assertTrue(in_array(resolve(YouTubeParser::class), $environment->getInlineParsers(), true)); + $this->assertTrue(in_array(resolve(GistParser::class), $environment->getInlineParsers(), true)); + $this->assertTrue(in_array(resolve(CodepenParser::class), $environment->getInlineParsers(), true)); $this->assertTrue(in_array(resolve(ColorParser::class), $environment->getBlockParsers(), true)); } }