]> _ Git - pmi.git/commitdiff
Add automatic YouTube embeds to markdown fields. Done #5024
authorStephen Cameron <stephen@cubedesigners.com>
Thu, 21 Apr 2022 16:19:23 +0000 (18:19 +0200)
committerStephen Cameron <stephen@cubedesigners.com>
Thu, 21 Apr 2022 16:19:23 +0000 (18:19 +0200)
app/Markdown/YouTube/Element.php [new file with mode: 0644]
app/Markdown/YouTube/Extension.php [new file with mode: 0644]
app/Markdown/YouTube/Parser.php [new file with mode: 0644]
app/Markdown/YouTube/Renderer.php [new file with mode: 0644]
composer.json
composer.lock
config/markdown.php

diff --git a/app/Markdown/YouTube/Element.php b/app/Markdown/YouTube/Element.php
new file mode 100644 (file)
index 0000000..1423a0e
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Markdown\YouTube;
+
+use League\CommonMark\Block\Element\AbstractBlock;
+use League\CommonMark\Cursor;
+
+
+class Element extends AbstractBlock {
+
+    private $URL;
+
+    public function __construct($URL) {
+        $this->URL = $URL;
+    }
+
+    public function getURL() {
+        return $this->URL;
+    }
+
+    public function getVideoID() {
+
+        // Extract the video ID from the URLs. This should support all the main YouTube URL formats
+        // Instead of regex, we could use parse_url() here - it's more readable / maintainable but also harder
+        // to support multiple URL formats. See example:
+        // https://github.com/zoonru/commonmark-ext-youtube-iframe/blob/master/src/YouTubeLongUrlParser.php
+        preg_match("/(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([\w\-_]+)/", $this->URL, $matches);
+        if ($matches[1]) {
+            return $matches[1];
+        }
+        return null;
+    }
+
+    public function canContain(AbstractBlock $block): bool {
+        return false;
+    }
+
+    public function acceptsLines(): bool {
+        return false;
+    }
+
+    public function isCode(): bool {
+        return false;
+    }
+
+    public function shouldLastLineBeBlank(Cursor $cursor, $currentLineNumber): bool {
+        return false;
+    }
+
+    public function matchesNextLine(Cursor $cursor): bool {
+        return false;
+    }
+
+}
diff --git a/app/Markdown/YouTube/Extension.php b/app/Markdown/YouTube/Extension.php
new file mode 100644 (file)
index 0000000..1a8fbe2
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Markdown\YouTube;
+
+use League\CommonMark\ConfigurableEnvironmentInterface;
+use League\CommonMark\Extension\ExtensionInterface;
+
+class Extension implements ExtensionInterface
+{
+    public function register(ConfigurableEnvironmentInterface $environment)
+    {
+        // This YouTube extension will find bare YouTube URLs that exist on their own line in the markdown
+        // and convert them to a lite-youtube embed (a bit like oEmbed).
+        // If we could use CommonMark 2.3+, we could use this extension instead, which does a lot more:
+        // https://commonmark.thephpleague.com/2.3/extensions/embed/
+        $environment
+            ->addBlockParser(new Parser())
+            ->addBlockRenderer(Element::class, new Renderer(
+                (string) $environment->getConfig('youtube_iframe_width', 560),
+                               (string) $environment->getConfig('youtube_iframe_height', 315),
+                               (bool) $environment->getConfig('youtube_iframe_allowfullscreen', true)
+            ));
+    }
+}
diff --git a/app/Markdown/YouTube/Parser.php b/app/Markdown/YouTube/Parser.php
new file mode 100644 (file)
index 0000000..ea1044b
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Markdown\YouTube;
+
+use League\CommonMark\Block\Parser\BlockParserInterface;
+use League\CommonMark\ContextInterface;
+use League\CommonMark\Cursor;
+
+class Parser implements BlockParserInterface {
+
+    public function parse(ContextInterface $context, Cursor $cursor): bool {
+        if ($cursor->isIndented()) {
+            return false;
+        }
+
+        // Make sure that we have a valid YouTube video URL to work with
+        $matched = $cursor->match("~(?:https?://)?(?:www\.)?youtu\.?be(?:\.com)?/?.*(?:watch|embed)?(?:.*v=|v/|/)([\w\-_]+)~");
+        if (!$matched) {
+            return false; // No match, let this line be handled by another parser
+        }
+        $context->addBlock(new Element($matched));
+
+        return true;
+    }
+
+}
diff --git a/app/Markdown/YouTube/Renderer.php b/app/Markdown/YouTube/Renderer.php
new file mode 100644 (file)
index 0000000..37ad7b2
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Markdown\YouTube;
+
+use League\CommonMark\Block\Element\AbstractBlock;
+use League\CommonMark\Block\Renderer\BlockRendererInterface;
+use League\CommonMark\ElementRendererInterface;
+use League\CommonMark\HtmlElement;
+
+class Renderer implements BlockRendererInterface {
+
+    private $width;
+    private $height;
+    private $allowFullScreen;
+
+    /**
+     * YouTubeIframeRenderer constructor.
+     * @param string $width
+     * @param string $height
+     * @param bool $allowFullScreen
+     */
+    public function __construct(string $width, string $height, bool $allowFullScreen) {
+        $this->width = $width;
+        $this->height = $height;
+        $this->allowFullScreen = $allowFullScreen;
+    }
+
+    public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, $inTightList = false): HtmlElement {
+
+        // Make sure we're rendering a YouTube Element
+        if (!($block instanceof Element)) {
+            throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
+        }
+
+        /* @var $block Element */
+        $video_ID = $block->getVideoID();
+
+        // Javascript to ensure the lite-youtube script is only loaded once...
+        // Note: This is problematic for sites with Vue wrappers (eg. PMI) because a raw <script> is not allowed inside
+        // the Vue app's content. Although not ideal, having the script tag included with each video instance doesn't
+        // seem to cause any issues...
+        // In the future, we should use the embed extension for CommonMark (https://commonmark.thephpleague.com/2.3/extensions/embed/)
+        // but this requires newer package versions than we can support currently.
+        // Alternatively, we could use the standard YouTube iframe but this is less performant.
+        // $selective_loader = '
+        //     if (!document.getElementById("liteYouTube") && customElements.get("lite-youtube") === undefined) {
+        //         const script = document.createElement("script");
+        //         script.src = "https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.3.1/lite-youtube.js";
+        //         script.id = "liteYouTube";
+        //         document.body.appendChild(script);
+        //     }
+        // ';
+
+        // TODO: instead of loading this each time, it should be done just once per page (see note above).
+        $script = new HtmlElement('script', ['src' => 'https://cdn.jsdelivr.net/npm/@justinribeiro/lite-youtube@1.3.1/lite-youtube.js', 'type' => 'module']);
+        $video = new HtmlElement('lite-youtube', ['videoid' => $video_ID]);
+
+        return new HtmlElement('p', ['class' => 'yt-wrapper'], $video . $script);
+    }
+
+}
index 5830d4d627159663ba1c405e4b596aa49ad37b50..6cf615584680ad23b6537ed304efefc9c52d839a 100644 (file)
     ],
     "license": "proprietary",
     "require": {
+        "ext-json": "*",
         "cubist/cms-back": "dev-backpack3.6",
         "league/csv": "^9.2",
         "nothingworks/blade-svg": "^0.3.1",
-        "spatie/laravel-blade-x": "^2.2",
-        "ext-json": "*"
+        "spatie/laravel-blade-x": "^2.2"
     },
     "config": {
         "optimize-autoloader": true,
index a159867da47fb445af78ea604c0f69d9ad34ca4c..81d3cd3c8a8b23747f01aca937c5052f7ae5319c 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "2684a572ce69945f107df7cadc2a92e1",
+    "content-hash": "40b6079df94541721a0f10d3a1575039",
     "packages": [
         {
             "name": "almasaeed2010/adminlte",
         "ext-json": "*"
     },
     "platform-dev": [],
-    "plugin-api-version": "2.0.0"
+    "plugin-api-version": "2.1.0"
 }
index 5f7115cc6b086bc8c04e24123a821af8ff274e7c..93c9604813bfceaa137a6e0c74d400111f71470e 100644 (file)
@@ -47,7 +47,7 @@ return [
         League\CommonMark\Ext\Autolink\AutolinkExtension::class,
         League\CommonMark\Ext\Table\TableExtension::class,
         Cubist\Backpack\app\Markdown\InternalLink\Extension::class,
-
+        App\Markdown\YouTube\Extension::class,
     ],
 
     /*