From 9c4f60e00bcbd4f784f12d2a6fed357ad402ee2e Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Wed, 2 Feb 2022 17:01:19 +0000
Subject: [PATCH] fix(markdown-editor): remove unnecessary buttons for podcast
 and episode editors + add extensions

update CommonMark to v2 + add Autolink, SmartPunct and DisallowedRawHtml extensions
---
 app/Entities/Episode.php                      |  23 +-
 app/Entities/Page.php                         |  22 +-
 app/Entities/Podcast.php                      |  39 +-
 app/Helpers/form_helper.php                   |  45 ++
 app/Views/Components/Forms/MarkdownEditor.php | 114 +++-
 composer.json                                 |   2 +-
 composer.lock                                 | 640 ++++++++++++------
 themes/cp_admin/episode/create.php            |   6 +-
 themes/cp_admin/episode/edit.php              |   6 +-
 themes/cp_admin/podcast/create.php            |   3 +-
 themes/cp_admin/podcast/edit.php              |   3 +-
 11 files changed, 627 insertions(+), 276 deletions(-)
 create mode 100644 app/Helpers/form_helper.php

diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 1b1c0e0f21..3f1d33ff42 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -26,7 +26,12 @@ use CodeIgniter\Entity\Entity;
 use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use CodeIgniter\I18n\Time;
-use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\Autolink\AutolinkExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
+use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
+use League\CommonMark\MarkdownConverter;
 use RuntimeException;
 
 /**
@@ -473,13 +478,21 @@ class Episode extends Entity
 
     public function setDescriptionMarkdown(string $descriptionMarkdown): static
     {
-        $converter = new CommonMarkConverter([
-            'html_input' => 'strip',
+        $config = [
+            'html_input' => 'escape',
             'allow_unsafe_links' => false,
-        ]);
+        ];
+
+        $environment = new Environment($config);
+        $environment->addExtension(new CommonMarkCoreExtension());
+        $environment->addExtension(new AutolinkExtension());
+        $environment->addExtension(new SmartPunctExtension());
+        $environment->addExtension(new DisallowedRawHtmlExtension());
+
+        $converter = new MarkdownConverter($environment);
 
         $this->attributes['description_markdown'] = $descriptionMarkdown;
-        $this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
+        $this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
 
         return $this;
     }
diff --git a/app/Entities/Page.php b/app/Entities/Page.php
index 7cf868a010..1628a787b3 100644
--- a/app/Entities/Page.php
+++ b/app/Entities/Page.php
@@ -12,7 +12,12 @@ namespace App\Entities;
 
 use CodeIgniter\Entity\Entity;
 use CodeIgniter\I18n\Time;
-use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\Autolink\AutolinkExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
+use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
+use League\CommonMark\MarkdownConverter;
 
 /**
  * @property int $id
@@ -49,13 +54,20 @@ class Page extends Entity
 
     public function setContentMarkdown(string $contentMarkdown): static
     {
-        $converter = new CommonMarkConverter([
-            'html_input' => 'strip',
+        $config = [
             'allow_unsafe_links' => false,
-        ]);
+        ];
+
+        $environment = new Environment($config);
+        $environment->addExtension(new CommonMarkCoreExtension());
+        $environment->addExtension(new AutolinkExtension());
+        $environment->addExtension(new SmartPunctExtension());
+        $environment->addExtension(new DisallowedRawHtmlExtension());
+
+        $converter = new MarkdownConverter($environment);
 
         $this->attributes['content_markdown'] = $contentMarkdown;
-        $this->attributes['content_html'] = $converter->convertToHtml($contentMarkdown);
+        $this->attributes['content_html'] = $converter->convert($contentMarkdown);
 
         return $this;
     }
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index 55a2ec3c0c..28a1e3fe5c 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -23,7 +23,12 @@ use CodeIgniter\Entity\Entity;
 use CodeIgniter\Files\File;
 use CodeIgniter\HTTP\Files\UploadedFile;
 use CodeIgniter\I18n\Time;
-use League\CommonMark\CommonMarkConverter;
+use League\CommonMark\Environment\Environment;
+use League\CommonMark\Extension\Autolink\AutolinkExtension;
+use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
+use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
+use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
+use League\CommonMark\MarkdownConverter;
 use Modules\Auth\Entities\User;
 use RuntimeException;
 
@@ -375,13 +380,21 @@ class Podcast extends Entity
 
     public function setDescriptionMarkdown(string $descriptionMarkdown): static
     {
-        $converter = new CommonMarkConverter([
-            'html_input' => 'strip',
+        $config = [
+            'html_input' => 'escape',
             'allow_unsafe_links' => false,
-        ]);
+        ];
+
+        $environment = new Environment($config);
+        $environment->addExtension(new CommonMarkCoreExtension());
+        $environment->addExtension(new AutolinkExtension());
+        $environment->addExtension(new SmartPunctExtension());
+        $environment->addExtension(new DisallowedRawHtmlExtension());
+
+        $converter = new MarkdownConverter($environment);
 
         $this->attributes['description_markdown'] = $descriptionMarkdown;
-        $this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
+        $this->attributes['description_html'] = $converter->convert($descriptionMarkdown);
 
         return $this;
     }
@@ -399,17 +412,25 @@ class Podcast extends Entity
             return $this;
         }
 
-        $converter = new CommonMarkConverter([
-            'html_input' => 'strip',
+        $config = [
+            'html_input' => 'escape',
             'allow_unsafe_links' => false,
-        ]);
+        ];
+
+        $environment = new Environment($config);
+        $environment->addExtension(new CommonMarkCoreExtension());
+        $environment->addExtension(new AutolinkExtension());
+        $environment->addExtension(new SmartPunctExtension());
+        $environment->addExtension(new DisallowedRawHtmlExtension());
+
+        $converter = new MarkdownConverter($environment);
 
         $this->attributes[
             'episode_description_footer_markdown'
         ] = $episodeDescriptionFooterMarkdown;
         $this->attributes[
             'episode_description_footer_html'
-        ] = $converter->convertToHtml($episodeDescriptionFooterMarkdown);
+        ] = $converter->convert($episodeDescriptionFooterMarkdown);
 
         return $this;
     }
diff --git a/app/Helpers/form_helper.php b/app/Helpers/form_helper.php
new file mode 100644
index 0000000000..aedc6db3db
--- /dev/null
+++ b/app/Helpers/form_helper.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+if (! function_exists('form_markdown_textarea')) {
+    /**
+     * Textarea field
+     *
+     * @param mixed $data
+     * @param mixed $extra
+     */
+    function form_markdown_textarea($data = '', string $value = '', $extra = ''): string
+    {
+        $defaults = [
+            'name' => is_array($data) ? '' : $data,
+            'cols' => '40',
+            'rows' => '10',
+        ];
+        if (! is_array($data) || ! isset($data['value'])) {
+            $val = $value;
+        } else {
+            $val = $data['value'];
+            unset($data['value']); // textareas don't use the value attribute
+        }
+
+        // Unsets default rows and cols if defined in extra field as array or string.
+        if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && stripos(
+            preg_replace('~\s+~', '', $extra),
+            'rows='
+        ) !== false)) {
+            unset($defaults['rows']);
+        }
+
+        if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && stripos(
+            preg_replace('~\s+~', '', $extra),
+            'cols='
+        ) !== false)) {
+            unset($defaults['cols']);
+        }
+
+        return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes($extra) . '>'
+                . $val
+                . "</textarea>\n";
+    }
+}
diff --git a/app/Views/Components/Forms/MarkdownEditor.php b/app/Views/Components/Forms/MarkdownEditor.php
index 9fab63a06a..66fb54c277 100644
--- a/app/Views/Components/Forms/MarkdownEditor.php
+++ b/app/Views/Components/Forms/MarkdownEditor.php
@@ -6,6 +6,16 @@ namespace App\Views\Components\Forms;
 
 class MarkdownEditor extends FormComponent
 {
+    /**
+     * @var string[]
+     */
+    protected array $disallowList = [];
+
+    public function setDisallowList(string $value): void
+    {
+        $this->disallowList = explode(',', $value);
+    }
+
     public function render(): string
     {
         $editorClass = 'w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent ' . $this->class;
@@ -13,30 +23,83 @@ class MarkdownEditor extends FormComponent
         $this->attributes['class'] = 'bg-elevated border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
         $this->attributes['rows'] = 6;
 
-        // dd(htmlspecialchars_decode($this->value));
         $value = htmlspecialchars_decode($this->value);
 
-        $textarea = form_textarea($this->attributes, old($this->name, $value, false));
-        $icons = [
-            'heading' => icon('heading'),
-            'bold' => icon('bold'),
-            'italic' => icon('italic'),
-            'list-unordered' => icon('list-unordered'),
-            'list-ordered' => icon('list-ordered'),
-            'quote' => icon('quote'),
-            'link' => icon('link'),
-            'image-add' => icon('image-add'),
-            'markdown' => icon(
-                'markdown',
-                'mr-1 text-lg opacity-40'
-            ),
-        ];
+        $oldValue = old($this->name);
+        if ($oldValue === null) {
+            $oldValue = $value;
+        }
+        $textarea = form_textarea($this->attributes, $oldValue);
+        $markdownIcon = icon(
+            'markdown',
+            'mr-1 text-lg opacity-40'
+        );
         $translations = [
             'write' => lang('Common.forms.editor.write'),
             'preview' => lang('Common.forms.editor.preview'),
             'help' => lang('Common.forms.editor.help'),
         ];
 
+        $toolbarGroups = [
+            [
+                [
+                    'name' => 'header',
+                    'tag' => 'md-header',
+                    'icon' => icon('heading'),
+                ],
+                [
+                    'name' => 'bold',
+                    'tag' => 'md-bold',
+                    'icon' => icon('bold'),
+                ],
+                [
+                    'name' => 'italic',
+                    'tag' => 'md-italic',
+                    'icon' => icon('italic'),
+                ],
+            ],
+            [
+                [
+                    'name' => 'unordered-list',
+                    'tag' => 'md-unordered-list',
+                    'icon' => icon('list-unordered'),
+                ],
+                [
+                    'name' => 'ordered-list',
+                    'tag' => 'md-ordered-list ',
+                    'icon' => icon('list-ordered'),
+                ],
+            ],
+            [
+                [
+                    'name' => 'quote',
+                    'tag' => 'md-quote',
+                    'icon' => icon('quote'),
+                ],
+                [
+                    'name' => 'link',
+                    'tag' => 'md-link',
+                    'icon' => icon('link'),
+                ],
+                [
+                    'name' => 'image',
+                    'tag' => 'md-image',
+                    'icon' => icon('image-add'),
+                ],
+            ],
+        ];
+
+        $toolbarContent = '';
+        foreach ($toolbarGroups as $buttonsGroup) {
+            $toolbarContent .= '<div class="inline-flex text-2xl gap-x-1">';
+            foreach ($buttonsGroup as $button) {
+                if (! in_array($button['name'], $this->disallowList, true)) {
+                    $toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">' . $button['icon'] . '</' . $button['tag'] . '>';
+                }
+            }
+            $toolbarContent .= '</div>';
+        }
+
         return <<<HTML
             <div class="{$editorClass}">
                 <header class="px-2">
@@ -45,22 +108,7 @@ class MarkdownEditor extends FormComponent
                             <button type="button" slot="write" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['write']}</button>
                             <button type="button" slot="preview" class="px-2 font-semibold focus:ring-inset focus:ring-accent">{$translations['preview']}</button>
                         </markdown-write-preview>
-                        <markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">
-                            <div class="inline-flex text-2xl gap-x-1">
-                                <md-header class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">{$icons['heading']}</md-header>
-                                <md-bold class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100" data-hotkey-scope="{$this->id}" data-hotkey="Control+b,Meta+b">{$icons['bold']}</md-bold>
-                                <md-italic class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100" data-hotkey-scope="{$this->id}" data-hotkey="Control+i,Meta+i">{$icons['italic']}</md-italic>
-                            </div>
-                            <div class="inline-flex text-2xl gap-x-1">
-                                <md-unordered-list class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">{$icons['list-unordered']}</md-unordered-list>
-                                <md-ordered-list class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">{$icons['list-ordered']}</md-ordered-list>
-                            </div>
-                            <div class="inline-flex text-2xl gap-x-1">
-                                <md-quote class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">{$icons['quote']}</md-quote>
-                                <md-link class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100" data-hotkey-scope="{$this->id}" data-hotkey="Control+k,Meta+k">{$icons['link']}</md-link>
-                                <md-image class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">{$icons['image-add']}</md-image>
-                            </div>
-                        </markdown-toolbar>
+                        <markdown-toolbar for="{$this->id}" class="flex gap-4 px-2 py-1">{$toolbarContent}</markdown-toolbar>
                     </div>
                 </header>
                 <div class="relative">
@@ -68,7 +116,7 @@ class MarkdownEditor extends FormComponent
                     <markdown-preview for="{$this->id}" class="absolute top-0 left-0 hidden w-full h-full max-w-full px-3 py-2 overflow-y-auto prose bg-base" showClass="bg-elevated" />
                 </div>
                 <footer class="flex px-2 py-1 border-t bg-base">
-                    <a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-skin-muted hover:text-skin-base" target="_blank" rel="noopener noreferrer">{$icons['markdown']}{$translations['help']}</a>
+                    <a href="https://commonmark.org/help/" class="inline-flex items-center text-xs font-semibold text-skin-muted hover:text-skin-base" target="_blank" rel="noopener noreferrer">{$markdownIcon}{$translations['help']}</a>
                 </footer>
             </div>
         HTML;
diff --git a/composer.json b/composer.json
index 634f87c000..e3d8559082 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
     "geoip2/geoip2": "^v2.11.0",
     "myth/auth": "dev-develop",
     "codeigniter4/codeigniter4": "dev-develop",
-    "league/commonmark": "^v1.6.6",
+    "league/commonmark": "^2.2",
     "vlucas/phpdotenv": "^v5.3.0",
     "league/html-to-markdown": "^v5.0.1",
     "opawg/user-agents-php": "^v1.0",
diff --git a/composer.lock b/composer.lock
index d55a4c9d0e..91ca6af6af 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": "1cd09e20009835bd1480554991ad2ff1",
+  "content-hash": "bf66875f72680d0f5953f03bf31c8612",
   "packages": [
     {
       "name": "brick/math",
@@ -307,6 +307,74 @@
       ],
       "time": "2021-06-07T13:58:28+00:00"
     },
+    {
+      "name": "dflydev/dot-access-data",
+      "version": "v3.0.1",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/dflydev/dflydev-dot-access-data.git",
+        "reference": "0992cc19268b259a39e86f296da5f0677841f42c"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/0992cc19268b259a39e86f296da5f0677841f42c",
+        "reference": "0992cc19268b259a39e86f296da5f0677841f42c",
+        "shasum": ""
+      },
+      "require": {
+        "php": "^7.1 || ^8.0"
+      },
+      "require-dev": {
+        "phpstan/phpstan": "^0.12.42",
+        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
+        "scrutinizer/ocular": "1.6.0",
+        "squizlabs/php_codesniffer": "^3.5",
+        "vimeo/psalm": "^3.14"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-main": "3.x-dev"
+        }
+      },
+      "autoload": {
+        "psr-4": {
+          "Dflydev\\DotAccessData\\": "src/"
+        }
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["MIT"],
+      "authors": [
+        {
+          "name": "Dragonfly Development Inc.",
+          "email": "info@dflydev.com",
+          "homepage": "http://dflydev.com"
+        },
+        {
+          "name": "Beau Simensen",
+          "email": "beau@dflydev.com",
+          "homepage": "http://beausimensen.com"
+        },
+        {
+          "name": "Carlos Frutos",
+          "email": "carlos@kiwing.it",
+          "homepage": "https://github.com/cfrutos"
+        },
+        {
+          "name": "Colin O'Dell",
+          "email": "colinodell@gmail.com",
+          "homepage": "https://www.colinodell.com"
+        }
+      ],
+      "description": "Given a deep data structure, access data by dot notation.",
+      "homepage": "https://github.com/dflydev/dflydev-dot-access-data",
+      "keywords": ["access", "data", "dot", "notation"],
+      "support": {
+        "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
+        "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.1"
+      },
+      "time": "2021-08-13T13:06:58+00:00"
+    },
     {
       "name": "essence/dom",
       "version": "1.0.0",
@@ -783,40 +851,52 @@
     },
     {
       "name": "league/commonmark",
-      "version": "1.6.6",
+      "version": "2.2.1",
       "source": {
         "type": "git",
         "url": "https://github.com/thephpleague/commonmark.git",
-        "reference": "c4228d11e30d7493c6836d20872f9582d8ba6dcf"
+        "reference": "f8afb78f087777b040e0ab8a6b6ca93f6fc3f18a"
       },
       "dist": {
         "type": "zip",
-        "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4228d11e30d7493c6836d20872f9582d8ba6dcf",
-        "reference": "c4228d11e30d7493c6836d20872f9582d8ba6dcf",
+        "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/f8afb78f087777b040e0ab8a6b6ca93f6fc3f18a",
+        "reference": "f8afb78f087777b040e0ab8a6b6ca93f6fc3f18a",
         "shasum": ""
       },
       "require": {
         "ext-mbstring": "*",
-        "php": "^7.1 || ^8.0"
-      },
-      "conflict": {
-        "scrutinizer/ocular": "1.7.*"
+        "league/config": "^1.1.1",
+        "php": "^7.4 || ^8.0",
+        "psr/event-dispatcher": "^1.0",
+        "symfony/deprecation-contracts": "^2.1 || ^3.0",
+        "symfony/polyfill-php80": "^1.15"
       },
       "require-dev": {
-        "cebe/markdown": "~1.0",
-        "commonmark/commonmark.js": "0.29.2",
-        "erusev/parsedown": "~1.0",
+        "cebe/markdown": "^1.0",
+        "commonmark/cmark": "0.30.0",
+        "commonmark/commonmark.js": "0.30.0",
+        "composer/package-versions-deprecated": "^1.8",
+        "erusev/parsedown": "^1.0",
         "ext-json": "*",
         "github/gfm": "0.29.0",
-        "michelf/php-markdown": "~1.4",
-        "mikehaertl/php-shellcommand": "^1.4",
-        "phpstan/phpstan": "^0.12.90",
-        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2",
-        "scrutinizer/ocular": "^1.5",
-        "symfony/finder": "^4.2"
+        "michelf/php-markdown": "^1.4",
+        "phpstan/phpstan": "^0.12.88 || ^1.0.0",
+        "phpunit/phpunit": "^9.5.5",
+        "scrutinizer/ocular": "^1.8.1",
+        "symfony/finder": "^5.3",
+        "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
+        "unleashedtech/php-coding-standard": "^3.1",
+        "vimeo/psalm": "^4.7.3"
+      },
+      "suggest": {
+        "symfony/yaml": "v2.3+ required if using the Front Matter extension"
       },
-      "bin": ["bin/commonmark"],
       "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-main": "2.3-dev"
+        }
+      },
       "autoload": {
         "psr-4": {
           "League\\CommonMark\\": "src"
@@ -832,7 +912,7 @@
           "role": "Lead Developer"
         }
       ],
-      "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)",
+      "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
       "homepage": "https://commonmark.thephpleague.com",
       "keywords": [
         "commonmark",
@@ -846,15 +926,12 @@
       ],
       "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"
       },
       "funding": [
-        {
-          "url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark",
-          "type": "custom"
-        },
         {
           "url": "https://www.colinodell.com/sponsor",
           "type": "custom"
@@ -867,16 +944,92 @@
           "url": "https://github.com/colinodell",
           "type": "github"
         },
-        {
-          "url": "https://www.patreon.com/colinodell",
-          "type": "patreon"
-        },
         {
           "url": "https://tidelift.com/funding/github/packagist/league/commonmark",
           "type": "tidelift"
         }
       ],
-      "time": "2021-07-17T17:13:23+00:00"
+      "time": "2022-01-25T14:37:33+00:00"
+    },
+    {
+      "name": "league/config",
+      "version": "v1.1.1",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/thephpleague/config.git",
+        "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
+        "reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
+        "shasum": ""
+      },
+      "require": {
+        "dflydev/dot-access-data": "^3.0.1",
+        "nette/schema": "^1.2",
+        "php": "^7.4 || ^8.0"
+      },
+      "require-dev": {
+        "phpstan/phpstan": "^0.12.90",
+        "phpunit/phpunit": "^9.5.5",
+        "scrutinizer/ocular": "^1.8.1",
+        "unleashedtech/php-coding-standard": "^3.1",
+        "vimeo/psalm": "^4.7.3"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-main": "1.2-dev"
+        }
+      },
+      "autoload": {
+        "psr-4": {
+          "League\\Config\\": "src"
+        }
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["BSD-3-Clause"],
+      "authors": [
+        {
+          "name": "Colin O'Dell",
+          "email": "colinodell@gmail.com",
+          "homepage": "https://www.colinodell.com",
+          "role": "Lead Developer"
+        }
+      ],
+      "description": "Define configuration arrays with strict schemas and access values with dot notation",
+      "homepage": "https://config.thephpleague.com",
+      "keywords": [
+        "array",
+        "config",
+        "configuration",
+        "dot",
+        "dot-access",
+        "nested",
+        "schema"
+      ],
+      "support": {
+        "docs": "https://config.thephpleague.com/",
+        "issues": "https://github.com/thephpleague/config/issues",
+        "rss": "https://github.com/thephpleague/config/releases.atom",
+        "source": "https://github.com/thephpleague/config"
+      },
+      "funding": [
+        {
+          "url": "https://www.colinodell.com/sponsor",
+          "type": "custom"
+        },
+        {
+          "url": "https://www.paypal.me/colinpodell/10.00",
+          "type": "custom"
+        },
+        {
+          "url": "https://github.com/colinodell",
+          "type": "github"
+        }
+      ],
+      "time": "2021-08-14T12:15:32+00:00"
     },
     {
       "name": "league/html-to-markdown",
@@ -1236,6 +1389,138 @@
       ],
       "time": "2021-06-10T04:25:01+00:00"
     },
+    {
+      "name": "nette/schema",
+      "version": "v1.2.2",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/nette/schema.git",
+        "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df",
+        "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df",
+        "shasum": ""
+      },
+      "require": {
+        "nette/utils": "^2.5.7 || ^3.1.5 ||  ^4.0",
+        "php": ">=7.1 <8.2"
+      },
+      "require-dev": {
+        "nette/tester": "^2.3 || ^2.4",
+        "phpstan/phpstan-nette": "^0.12",
+        "tracy/tracy": "^2.7"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-master": "1.2-dev"
+        }
+      },
+      "autoload": {
+        "classmap": ["src/"]
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
+      "authors": [
+        {
+          "name": "David Grudl",
+          "homepage": "https://davidgrudl.com"
+        },
+        {
+          "name": "Nette Community",
+          "homepage": "https://nette.org/contributors"
+        }
+      ],
+      "description": "📐 Nette Schema: validating data structures against a given Schema.",
+      "homepage": "https://nette.org",
+      "keywords": ["config", "nette"],
+      "support": {
+        "issues": "https://github.com/nette/schema/issues",
+        "source": "https://github.com/nette/schema/tree/v1.2.2"
+      },
+      "time": "2021-10-15T11:40:02+00:00"
+    },
+    {
+      "name": "nette/utils",
+      "version": "v3.2.2",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/nette/utils.git",
+        "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/nette/utils/zipball/967cfc4f9a1acd5f1058d76715a424c53343c20c",
+        "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c",
+        "shasum": ""
+      },
+      "require": {
+        "php": ">=7.2 <8.1"
+      },
+      "conflict": {
+        "nette/di": "<3.0.6"
+      },
+      "require-dev": {
+        "nette/tester": "~2.0",
+        "phpstan/phpstan": "^0.12",
+        "tracy/tracy": "^2.3"
+      },
+      "suggest": {
+        "ext-gd": "to use Image",
+        "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
+        "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
+        "ext-json": "to use Nette\\Utils\\Json",
+        "ext-mbstring": "to use Strings::lower() etc...",
+        "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
+        "ext-xml": "to use Strings::length() etc. when mbstring is not available"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-master": "3.2-dev"
+        }
+      },
+      "autoload": {
+        "classmap": ["src/"]
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
+      "authors": [
+        {
+          "name": "David Grudl",
+          "homepage": "https://davidgrudl.com"
+        },
+        {
+          "name": "Nette Community",
+          "homepage": "https://nette.org/contributors"
+        }
+      ],
+      "description": "🛠  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
+      "homepage": "https://nette.org",
+      "keywords": [
+        "array",
+        "core",
+        "datetime",
+        "images",
+        "json",
+        "nette",
+        "paginator",
+        "password",
+        "slugify",
+        "string",
+        "unicode",
+        "utf-8",
+        "utility",
+        "validation"
+      ],
+      "support": {
+        "issues": "https://github.com/nette/utils/issues",
+        "source": "https://github.com/nette/utils/tree/v3.2.2"
+      },
+      "time": "2021-03-03T22:53:25+00:00"
+    },
     {
       "name": "opawg/user-agents-php",
       "version": "v1.0",
@@ -1541,9 +1826,53 @@
       "description": "Common interface for caching libraries",
       "keywords": ["cache", "psr", "psr-6"],
       "support": {
-        "source": "https://github.com/php-fig/cache/tree/master"
+        "source": "https://github.com/php-fig/cache/tree/master"
+      },
+      "time": "2016-08-06T20:24:11+00:00"
+    },
+    {
+      "name": "psr/event-dispatcher",
+      "version": "1.0.0",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/php-fig/event-dispatcher.git",
+        "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+        "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+        "shasum": ""
+      },
+      "require": {
+        "php": ">=7.2.0"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-master": "1.0.x-dev"
+        }
+      },
+      "autoload": {
+        "psr-4": {
+          "Psr\\EventDispatcher\\": "src/"
+        }
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["MIT"],
+      "authors": [
+        {
+          "name": "PHP-FIG",
+          "homepage": "http://www.php-fig.org/"
+        }
+      ],
+      "description": "Standard interfaces for event handling.",
+      "keywords": ["events", "psr", "psr-14"],
+      "support": {
+        "issues": "https://github.com/php-fig/event-dispatcher/issues",
+        "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
       },
-      "time": "2016-08-06T20:24:11+00:00"
+      "time": "2019-01-08T18:20:26+00:00"
     },
     {
       "name": "psr/log",
@@ -1741,6 +2070,69 @@
       ],
       "time": "2020-08-18T17:17:46+00:00"
     },
+    {
+      "name": "symfony/deprecation-contracts",
+      "version": "v2.4.0",
+      "source": {
+        "type": "git",
+        "url": "https://github.com/symfony/deprecation-contracts.git",
+        "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
+      },
+      "dist": {
+        "type": "zip",
+        "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
+        "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
+        "shasum": ""
+      },
+      "require": {
+        "php": ">=7.1"
+      },
+      "type": "library",
+      "extra": {
+        "branch-alias": {
+          "dev-main": "2.4-dev"
+        },
+        "thanks": {
+          "name": "symfony/contracts",
+          "url": "https://github.com/symfony/contracts"
+        }
+      },
+      "autoload": {
+        "files": ["function.php"]
+      },
+      "notification-url": "https://packagist.org/downloads/",
+      "license": ["MIT"],
+      "authors": [
+        {
+          "name": "Nicolas Grekas",
+          "email": "p@tchwork.com"
+        },
+        {
+          "name": "Symfony Community",
+          "homepage": "https://symfony.com/contributors"
+        }
+      ],
+      "description": "A generic function and convention to trigger deprecation notices",
+      "homepage": "https://symfony.com",
+      "support": {
+        "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
+      },
+      "funding": [
+        {
+          "url": "https://symfony.com/sponsor",
+          "type": "custom"
+        },
+        {
+          "url": "https://github.com/fabpot",
+          "type": "github"
+        },
+        {
+          "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+          "type": "tidelift"
+        }
+      ],
+      "time": "2021-03-23T23:28:01+00:00"
+    },
     {
       "name": "symfony/polyfill-ctype",
       "version": "v1.23.0",
@@ -2787,85 +3179,6 @@
       },
       "time": "2021-02-28T12:30:32+00:00"
     },
-    {
-      "name": "nette/utils",
-      "version": "v3.2.2",
-      "source": {
-        "type": "git",
-        "url": "https://github.com/nette/utils.git",
-        "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c"
-      },
-      "dist": {
-        "type": "zip",
-        "url": "https://api.github.com/repos/nette/utils/zipball/967cfc4f9a1acd5f1058d76715a424c53343c20c",
-        "reference": "967cfc4f9a1acd5f1058d76715a424c53343c20c",
-        "shasum": ""
-      },
-      "require": {
-        "php": ">=7.2 <8.1"
-      },
-      "conflict": {
-        "nette/di": "<3.0.6"
-      },
-      "require-dev": {
-        "nette/tester": "~2.0",
-        "phpstan/phpstan": "^0.12",
-        "tracy/tracy": "^2.3"
-      },
-      "suggest": {
-        "ext-gd": "to use Image",
-        "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
-        "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
-        "ext-json": "to use Nette\\Utils\\Json",
-        "ext-mbstring": "to use Strings::lower() etc...",
-        "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
-        "ext-xml": "to use Strings::length() etc. when mbstring is not available"
-      },
-      "type": "library",
-      "extra": {
-        "branch-alias": {
-          "dev-master": "3.2-dev"
-        }
-      },
-      "autoload": {
-        "classmap": ["src/"]
-      },
-      "notification-url": "https://packagist.org/downloads/",
-      "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
-      "authors": [
-        {
-          "name": "David Grudl",
-          "homepage": "https://davidgrudl.com"
-        },
-        {
-          "name": "Nette Community",
-          "homepage": "https://nette.org/contributors"
-        }
-      ],
-      "description": "🛠  Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
-      "homepage": "https://nette.org",
-      "keywords": [
-        "array",
-        "core",
-        "datetime",
-        "images",
-        "json",
-        "nette",
-        "paginator",
-        "password",
-        "slugify",
-        "string",
-        "unicode",
-        "utf-8",
-        "utility",
-        "validation"
-      ],
-      "support": {
-        "issues": "https://github.com/nette/utils/issues",
-        "source": "https://github.com/nette/utils/tree/v3.2.2"
-      },
-      "time": "2021-03-03T22:53:25+00:00"
-    },
     {
       "name": "nikic/php-parser",
       "version": "v4.10.5",
@@ -3838,50 +4151,6 @@
       },
       "time": "2021-03-05T17:36:06+00:00"
     },
-    {
-      "name": "psr/event-dispatcher",
-      "version": "1.0.0",
-      "source": {
-        "type": "git",
-        "url": "https://github.com/php-fig/event-dispatcher.git",
-        "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
-      },
-      "dist": {
-        "type": "zip",
-        "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
-        "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
-        "shasum": ""
-      },
-      "require": {
-        "php": ">=7.2.0"
-      },
-      "type": "library",
-      "extra": {
-        "branch-alias": {
-          "dev-master": "1.0.x-dev"
-        }
-      },
-      "autoload": {
-        "psr-4": {
-          "Psr\\EventDispatcher\\": "src/"
-        }
-      },
-      "notification-url": "https://packagist.org/downloads/",
-      "license": ["MIT"],
-      "authors": [
-        {
-          "name": "PHP-FIG",
-          "homepage": "http://www.php-fig.org/"
-        }
-      ],
-      "description": "Standard interfaces for event handling.",
-      "keywords": ["events", "psr", "psr-14"],
-      "support": {
-        "issues": "https://github.com/php-fig/event-dispatcher/issues",
-        "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
-      },
-      "time": "2019-01-08T18:20:26+00:00"
-    },
     {
       "name": "rector/rector",
       "version": "0.11.16",
@@ -5271,69 +5540,6 @@
       ],
       "time": "2021-05-26T17:57:12+00:00"
     },
-    {
-      "name": "symfony/deprecation-contracts",
-      "version": "v2.4.0",
-      "source": {
-        "type": "git",
-        "url": "https://github.com/symfony/deprecation-contracts.git",
-        "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627"
-      },
-      "dist": {
-        "type": "zip",
-        "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627",
-        "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627",
-        "shasum": ""
-      },
-      "require": {
-        "php": ">=7.1"
-      },
-      "type": "library",
-      "extra": {
-        "branch-alias": {
-          "dev-main": "2.4-dev"
-        },
-        "thanks": {
-          "name": "symfony/contracts",
-          "url": "https://github.com/symfony/contracts"
-        }
-      },
-      "autoload": {
-        "files": ["function.php"]
-      },
-      "notification-url": "https://packagist.org/downloads/",
-      "license": ["MIT"],
-      "authors": [
-        {
-          "name": "Nicolas Grekas",
-          "email": "p@tchwork.com"
-        },
-        {
-          "name": "Symfony Community",
-          "homepage": "https://symfony.com/contributors"
-        }
-      ],
-      "description": "A generic function and convention to trigger deprecation notices",
-      "homepage": "https://symfony.com",
-      "support": {
-        "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0"
-      },
-      "funding": [
-        {
-          "url": "https://symfony.com/sponsor",
-          "type": "custom"
-        },
-        {
-          "url": "https://github.com/fabpot",
-          "type": "github"
-        },
-        {
-          "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-          "type": "tidelift"
-        }
-      ],
-      "time": "2021-03-23T23:28:01+00:00"
-    },
     {
       "name": "symfony/error-handler",
       "version": "v5.3.0",
diff --git a/themes/cp_admin/episode/create.php b/themes/cp_admin/episode/create.php
index bfc2d6115b..4bb1b89c23 100644
--- a/themes/cp_admin/episode/create.php
+++ b/themes/cp_admin/episode/create.php
@@ -118,13 +118,15 @@
     as="MarkdownEditor"
     name="description"
     label="<?= lang('Episode.form.description') ?>"
-    required="true" />
+    required="true"
+    disallowList="header,quote" />
 
 <Forms.Field
     as="MarkdownEditor"
     name="description_footer"
     label="<?= lang('Episode.form.description_footer') ?>"
-    hint="<?= lang('Episode.form.description_footer_hint') ?>" />
+    hint="<?= lang('Episode.form.description_footer_hint') ?>"
+    disallowList="header,quote" />
 
 </Forms.Section>
 
diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php
index 8f2cbb19b0..2ea9988d64 100644
--- a/themes/cp_admin/episode/edit.php
+++ b/themes/cp_admin/episode/edit.php
@@ -123,14 +123,16 @@
     name="description"
     label="<?= lang('Episode.form.description') ?>"
     value="<?= htmlspecialchars($episode->description_markdown) ?>"
-    required="true" />
+    required="true"
+    disallowList="header,quote" />
 
 <Forms.Field
     as="MarkdownEditor"
     name="description_footer"
     label="<?= lang('Episode.form.description_footer') ?>"
     hint="<?= lang('Episode.form.description_footer_hint') ?>"
-    value="<?= htmlspecialchars($podcast->episode_description_footer_markdown) ?? '' ?>" />
+    value="<?= htmlspecialchars($podcast->episode_description_footer_markdown) ?? '' ?>"
+    disallowList="header,quote" />
 
 </Forms.Section>
 
diff --git a/themes/cp_admin/podcast/create.php b/themes/cp_admin/podcast/create.php
index f42576cbef..3d4250e3fc 100644
--- a/themes/cp_admin/podcast/create.php
+++ b/themes/cp_admin/podcast/create.php
@@ -53,7 +53,8 @@
     as="MarkdownEditor"
     name="description"
     label="<?= lang('Podcast.form.description') ?>"
-    required="true" />
+    required="true"
+    disallowList="header,quote" />
 
 <fieldset>
     <legend><?= lang('Podcast.form.type.label') ?></legend>
diff --git a/themes/cp_admin/podcast/edit.php b/themes/cp_admin/podcast/edit.php
index a895d19ccd..2f6de9d2b9 100644
--- a/themes/cp_admin/podcast/edit.php
+++ b/themes/cp_admin/podcast/edit.php
@@ -68,7 +68,8 @@
     name="description"
     label="<?= lang('Podcast.form.description') ?>"
     value="<?= htmlspecialchars($podcast->description_markdown) ?>"
-    required="true" />
+    required="true"
+    disallowList="header,quote" />
 
 <fieldset>
     <legend><?= lang('Podcast.form.type.label') ?></legend>
-- 
GitLab