From f39938c4e3b8750668442a6a17e1006953b5cef2 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 12 Jun 2023 16:45:30 +0100
Subject: [PATCH 1/5] Added activity text for each activity type

Ensures some sensible text is always in webhook text data.
Also aligned some notification reporting to use centralised activity
system instead of custom success events.

For #4216
---
 app/Api/UserApiTokenController.php            |  3 --
 .../Controllers/ChapterController.php         |  2 -
 app/Entities/Controllers/PageController.php   |  4 +-
 .../Controllers/PageRevisionController.php    |  9 ++---
 .../Controllers/RecycleBinController.php      |  2 +-
 app/Settings/SettingController.php            |  2 -
 app/Users/Controllers/UserController.php      | 11 ++---
 lang/en/activities.php                        | 40 ++++++++++++++++++-
 lang/en/entities.php                          |  3 --
 lang/en/settings.php                          |  4 --
 tests/LanguageTest.php                        | 10 +++++
 tests/Permissions/RolesTest.php               |  2 +-
 12 files changed, 59 insertions(+), 33 deletions(-)

diff --git a/app/Api/UserApiTokenController.php b/app/Api/UserApiTokenController.php
index d8fc1171c..8357420ee 100644
--- a/app/Api/UserApiTokenController.php
+++ b/app/Api/UserApiTokenController.php
@@ -58,7 +58,6 @@ class UserApiTokenController extends Controller
         $token->save();
 
         session()->flash('api-token-secret:' . $token->id, $secret);
-        $this->showSuccessNotification(trans('settings.user_api_token_create_success'));
         $this->logActivity(ActivityType::API_TOKEN_CREATE, $token);
 
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
@@ -96,7 +95,6 @@ class UserApiTokenController extends Controller
             'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(),
         ])->save();
 
-        $this->showSuccessNotification(trans('settings.user_api_token_update_success'));
         $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token);
 
         return redirect($user->getEditUrl('/api-tokens/' . $token->id));
@@ -123,7 +121,6 @@ class UserApiTokenController extends Controller
         [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId);
         $token->delete();
 
-        $this->showSuccessNotification(trans('settings.user_api_token_delete_success'));
         $this->logActivity(ActivityType::API_TOKEN_DELETE, $token);
 
         return redirect($user->getEditUrl('#api_tokens'));
diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php
index cf7611685..7dcb66903 100644
--- a/app/Entities/Controllers/ChapterController.php
+++ b/app/Entities/Controllers/ChapterController.php
@@ -191,8 +191,6 @@ class ChapterController extends Controller
             return redirect()->back();
         }
 
-        $this->showSuccessNotification(trans('entities.chapter_move_success', ['bookName' => $newBook->name]));
-
         return redirect($chapter->getUrl());
     }
 
diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php
index e0444ecd2..3187e6486 100644
--- a/app/Entities/Controllers/PageController.php
+++ b/app/Entities/Controllers/PageController.php
@@ -389,7 +389,7 @@ class PageController extends Controller
         }
 
         try {
-            $parent = $this->pageRepo->move($page, $entitySelection);
+            $this->pageRepo->move($page, $entitySelection);
         } catch (PermissionsException $exception) {
             $this->showPermissionError();
         } catch (Exception $exception) {
@@ -398,8 +398,6 @@ class PageController extends Controller
             return redirect()->back();
         }
 
-        $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
-
         return redirect($page->getUrl());
     }
 
diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php
index a723513a8..9e6a90477 100644
--- a/app/Entities/Controllers/PageRevisionController.php
+++ b/app/Entities/Controllers/PageRevisionController.php
@@ -15,11 +15,9 @@ use Ssddanbrown\HtmlDiff\Diff;
 
 class PageRevisionController extends Controller
 {
-    protected PageRepo $pageRepo;
-
-    public function __construct(PageRepo $pageRepo)
-    {
-        $this->pageRepo = $pageRepo;
+    public function __construct(
+        protected PageRepo $pageRepo
+    ) {
     }
 
     /**
@@ -153,7 +151,6 @@ class PageRevisionController extends Controller
 
         $revision->delete();
         Activity::add(ActivityType::REVISION_DELETE, $revision);
-        $this->showSuccessNotification(trans('entities.revision_delete_success'));
 
         return redirect($page->getUrl('/revisions'));
     }
diff --git a/app/Entities/Controllers/RecycleBinController.php b/app/Entities/Controllers/RecycleBinController.php
index 30b184bbe..78f86a5ae 100644
--- a/app/Entities/Controllers/RecycleBinController.php
+++ b/app/Entities/Controllers/RecycleBinController.php
@@ -11,7 +11,7 @@ use BookStack\Http\Controller;
 
 class RecycleBinController extends Controller
 {
-    protected $recycleBinBaseUrl = '/settings/recycle-bin';
+    protected string $recycleBinBaseUrl = '/settings/recycle-bin';
 
     /**
      * On each request to a method of this controller check permissions
diff --git a/app/Settings/SettingController.php b/app/Settings/SettingController.php
index ffdd7545e..bd55222f2 100644
--- a/app/Settings/SettingController.php
+++ b/app/Settings/SettingController.php
@@ -52,9 +52,7 @@ class SettingController extends Controller
         ]);
 
         $store->storeFromUpdateRequest($request, $category);
-
         $this->logActivity(ActivityType::SETTINGS_UPDATE, $category);
-        $this->showSuccessNotification(trans('settings.settings_save_success'));
 
         return redirect("/settings/{$category}");
     }
diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php
index b185f0856..1c1b7ba23 100644
--- a/app/Users/Controllers/UserController.php
+++ b/app/Users/Controllers/UserController.php
@@ -19,13 +19,10 @@ use Illuminate\Validation\ValidationException;
 
 class UserController extends Controller
 {
-    protected UserRepo $userRepo;
-    protected ImageRepo $imageRepo;
-
-    public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
-    {
-        $this->userRepo = $userRepo;
-        $this->imageRepo = $imageRepo;
+    public function __construct(
+        protected UserRepo $userRepo,
+        protected ImageRepo $imageRepo
+    ) {
     }
 
     /**
diff --git a/lang/en/activities.php b/lang/en/activities.php
index e89b8eab2..e71a490de 100644
--- a/lang/en/activities.php
+++ b/lang/en/activities.php
@@ -15,6 +15,7 @@ return [
     'page_restore'                => 'restored page',
     'page_restore_notification'   => 'Page successfully restored',
     'page_move'                   => 'moved page',
+    'page_move_notification'      => 'Page successfully moved',
 
     // Chapters
     'chapter_create'              => 'created chapter',
@@ -24,6 +25,7 @@ return [
     'chapter_delete'              => 'deleted chapter',
     'chapter_delete_notification' => 'Chapter successfully deleted',
     'chapter_move'                => 'moved chapter',
+    'chapter_move_notification' => 'Chapter successfully moved',
 
     // Books
     'book_create'                 => 'created book',
@@ -47,14 +49,30 @@ return [
     'bookshelf_delete'                 => 'deleted shelf',
     'bookshelf_delete_notification'    => 'Shelf successfully deleted',
 
+    // Revisions
+    'revision_restore' => 'restored revision',
+    'revision_delete' => 'deleted revision',
+    'revision_delete_notification' => 'Revision successfully deleted',
+
     // Favourites
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
-    // MFA
+    // Auth
+    'auth_login' => 'logged in',
+    'auth_register' => 'registered as new user',
+    'auth_password_reset_request' => 'requested user password reset',
+    'auth_password_reset_update' => 'reset user password',
+    'mfa_setup_method' => 'configured MFA method',
     'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method' => 'removed MFA method',
     'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
 
+    // Settings
+    'settings_update' => 'updated settings',
+    'settings_update_notification' => 'Settings successfully updated',
+    'maintenance_action_run' => 'ran maintenance action',
+
     // Webhooks
     'webhook_create' => 'created webhook',
     'webhook_create_notification' => 'Webhook successfully created',
@@ -64,14 +82,34 @@ return [
     'webhook_delete_notification' => 'Webhook successfully deleted',
 
     // Users
+    'user_create' => 'created user',
+    'user_create_notification' => 'User successfully created',
+    'user_update' => 'updated user',
     'user_update_notification' => 'User successfully updated',
+    'user_delete' => 'deleted user',
     'user_delete_notification' => 'User successfully removed',
 
+    // API Tokens
+    'api_token_create' => 'created api token',
+    'api_token_create_notification' => 'API token successfully created',
+    'api_token_update' => 'updated api token',
+    'api_token_update_notification' => 'API token successfully updated',
+    'api_token_delete' => 'deleted api token',
+    'api_token_delete_notification' => 'API token successfully deleted',
+
     // Roles
+    'role_create' => 'created role',
     'role_create_notification' => 'Role successfully created',
+    'role_update' => 'updated role',
     'role_update_notification' => 'Role successfully updated',
+    'role_delete' => 'deleted role',
     'role_delete_notification' => 'Role successfully deleted',
 
+    // Recycle Bin
+    'recycle_bin_empty' => 'emptied recycle bin',
+    'recycle_bin_restore' => 'restored from recycle bin',
+    'recycle_bin_destroy' => 'removed from recycle bin',
+
     // Other
     'commented_on'                => 'commented on',
     'permissions_update'          => 'updated permissions',
diff --git a/lang/en/entities.php b/lang/en/entities.php
index caf9e2361..92903ed1f 100644
--- a/lang/en/entities.php
+++ b/lang/en/entities.php
@@ -180,7 +180,6 @@ return [
     'chapters_save' => 'Save Chapter',
     'chapters_move' => 'Move Chapter',
     'chapters_move_named' => 'Move Chapter :chapterName',
-    'chapter_move_success' => 'Chapter moved to :bookName',
     'chapters_copy' => 'Copy Chapter',
     'chapters_copy_success' => 'Chapter successfully copied',
     'chapters_permissions' => 'Chapter Permissions',
@@ -240,7 +239,6 @@ return [
     'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
-    'pages_move_success' => 'Page moved to ":parentName"',
     'pages_copy' => 'Copy Page',
     'pages_copy_desination' => 'Copy Destination',
     'pages_copy_success' => 'Page successfully copied',
@@ -375,7 +373,6 @@ return [
     // Revision
     'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
     'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
-    'revision_delete_success' => 'Revision deleted',
     'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
 
     // Copy view
diff --git a/lang/en/settings.php b/lang/en/settings.php
index 38d817915..c110e8992 100644
--- a/lang/en/settings.php
+++ b/lang/en/settings.php
@@ -9,7 +9,6 @@ return [
     // Common Messages
     'settings' => 'Settings',
     'settings_save' => 'Save Settings',
-    'settings_save_success' => 'Settings saved',
     'system_version' => 'System Version',
     'categories' => 'Categories',
 
@@ -232,8 +231,6 @@ return [
     'user_api_token_expiry' => 'Expiry Date',
     'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
     'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
-    'user_api_token_create_success' => 'API token successfully created',
-    'user_api_token_update_success' => 'API token successfully updated',
     'user_api_token' => 'API Token',
     'user_api_token_id' => 'Token ID',
     'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
@@ -244,7 +241,6 @@ return [
     'user_api_token_delete' => 'Delete Token',
     'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
     'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
-    'user_api_token_delete_success' => 'API token successfully deleted',
 
     // Webhooks
     'webhooks' => 'Webhooks',
diff --git a/tests/LanguageTest.php b/tests/LanguageTest.php
index b65227dd8..a66227ff2 100644
--- a/tests/LanguageTest.php
+++ b/tests/LanguageTest.php
@@ -2,6 +2,8 @@
 
 namespace Tests;
 
+use BookStack\Activity\ActivityType;
+
 class LanguageTest extends TestCase
 {
     protected array $langs;
@@ -90,4 +92,12 @@ class LanguageTest extends TestCase
         $loginReq->assertOk();
         $loginReq->assertSee('Log In');
     }
+
+    public function test_all_activity_types_have_activity_text()
+    {
+        foreach (ActivityType::all() as $activityType) {
+            $langKey = 'activities.' . $activityType;
+            $this->assertNotEquals($langKey, trans($langKey, [], 'en'));
+        }
+    }
 }
diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php
index dafa1f2bb..4c8ea1ab4 100644
--- a/tests/Permissions/RolesTest.php
+++ b/tests/Permissions/RolesTest.php
@@ -301,7 +301,7 @@ class RolesTest extends TestCase
         $resp = $this->post('/settings/features', []);
         $resp->assertRedirect('/settings/features');
         $resp = $this->get('/settings/features');
-        $resp->assertSee('Settings saved');
+        $resp->assertSee('Settings successfully updated');
     }
 
     public function test_restrictions_manage_all_permission()

From b01bbf9c8900ee2cc182f47d34aaa682bf1aa244 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Tue, 13 Jun 2023 15:13:07 +0100
Subject: [PATCH 2/5] Page Drafts: Added new "Delete Draft" action to draft
 menu

Provides a way for users to actually delte their user drafts where
required.
For #3927

Added test to cover new endpoint.

Makes update to MD editor #setText so that new selection is within new
range, otherwise it errors and fails operation.
---
 .../Controllers/PageRevisionController.php    | 15 ++++++-
 lang/en/entities.php                          |  4 +-
 lang/en/errors.php                            |  1 +
 resources/js/components/page-editor.js        | 42 +++++++++++++++----
 resources/js/markdown/actions.js              |  4 +-
 .../pages/parts/editor-toolbar.blade.php      | 13 +++++-
 resources/views/pages/parts/form.blade.php    | 11 ++++-
 routes/web.php                                |  1 +
 tests/Entity/PageDraftTest.php                | 24 +++++++++++
 9 files changed, 101 insertions(+), 14 deletions(-)

diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php
index 9e6a90477..a3190a0fc 100644
--- a/app/Entities/Controllers/PageRevisionController.php
+++ b/app/Entities/Controllers/PageRevisionController.php
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Controllers;
 use BookStack\Activity\ActivityType;
 use BookStack\Entities\Models\PageRevision;
 use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Repos\RevisionRepo;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
@@ -16,7 +17,8 @@ use Ssddanbrown\HtmlDiff\Diff;
 class PageRevisionController extends Controller
 {
     public function __construct(
-        protected PageRepo $pageRepo
+        protected PageRepo $pageRepo,
+        protected RevisionRepo $revisionRepo,
     ) {
     }
 
@@ -154,4 +156,15 @@ class PageRevisionController extends Controller
 
         return redirect($page->getUrl('/revisions'));
     }
+
+    /**
+     * Destroys existing drafts, belonging to the current user, for the given page.
+     */
+    public function destroyUserDraft(string $pageId)
+    {
+        $page = $this->pageRepo->getById($pageId);
+        $this->revisionRepo->deleteDraftsForCurrentUser($page);
+
+        return response('', 200);
+    }
 }
diff --git a/lang/en/entities.php b/lang/en/entities.php
index 92903ed1f..5a148e1a2 100644
--- a/lang/en/entities.php
+++ b/lang/en/entities.php
@@ -213,6 +213,7 @@ return [
     'pages_editing_page' => 'Editing Page',
     'pages_edit_draft_save_at' => 'Draft saved at ',
     'pages_edit_delete_draft' => 'Delete Draft',
+    'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
     'pages_edit_discard_draft' => 'Discard Draft',
     'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
     'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
@@ -285,7 +286,8 @@ return [
         'time_b' => 'in the last :minCount minutes',
         'message' => ':start :time. Take care not to overwrite each other\'s updates!',
     ],
-    'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
+    'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
+    'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
     'pages_specific' => 'Specific Page',
     'pages_is_template' => 'Page Template',
 
diff --git a/lang/en/errors.php b/lang/en/errors.php
index b03fb8c35..23c326f9e 100644
--- a/lang/en/errors.php
+++ b/lang/en/errors.php
@@ -58,6 +58,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
+    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
     'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
 
     // Entities
diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js
index e7f4c0ba9..963c21008 100644
--- a/resources/js/components/page-editor.js
+++ b/resources/js/components/page-editor.js
@@ -19,18 +19,23 @@ export class PageEditor extends Component {
         this.saveDraftButton = this.$refs.saveDraft;
         this.discardDraftButton = this.$refs.discardDraft;
         this.discardDraftWrap = this.$refs.discardDraftWrap;
+        this.deleteDraftButton = this.$refs.deleteDraft;
+        this.deleteDraftWrap = this.$refs.deleteDraftWrap;
         this.draftDisplay = this.$refs.draftDisplay;
         this.draftDisplayIcon = this.$refs.draftDisplayIcon;
         this.changelogInput = this.$refs.changelogInput;
         this.changelogDisplay = this.$refs.changelogDisplay;
         this.changeEditorButtons = this.$manyRefs.changeEditor || [];
         this.switchDialogContainer = this.$refs.switchDialog;
+        this.deleteDraftDialogContainer = this.$refs.deleteDraftDialog;
 
         // Translations
         this.draftText = this.$opts.draftText;
         this.autosaveFailText = this.$opts.autosaveFailText;
         this.editingPageText = this.$opts.editingPageText;
         this.draftDiscardedText = this.$opts.draftDiscardedText;
+        this.draftDeleteText = this.$opts.draftDeleteText;
+        this.draftDeleteFailText = this.$opts.draftDeleteFailText;
         this.setChangelogText = this.$opts.setChangelogText;
 
         // State data
@@ -75,6 +80,7 @@ export class PageEditor extends Component {
         // Draft Controls
         onSelect(this.saveDraftButton, this.saveDraft.bind(this));
         onSelect(this.discardDraftButton, this.discardDraft.bind(this));
+        onSelect(this.deleteDraftButton, this.deleteDraft.bind(this));
 
         // Change editor controls
         onSelect(this.changeEditorButtons, this.changeEditor.bind(this));
@@ -119,7 +125,8 @@ export class PageEditor extends Component {
         try {
             const resp = await window.$http.put(`/ajax/page/${this.pageId}/save-draft`, data);
             if (!this.isNewDraft) {
-                this.toggleDiscardDraftVisibility(true);
+                this.discardDraftWrap.toggleAttribute('hidden', false);
+                this.deleteDraftWrap.toggleAttribute('hidden', false);
             }
 
             this.draftNotifyChange(`${resp.data.message} ${Dates.utcTimeStampToLocalTime(resp.data.timestamp)}`);
@@ -154,7 +161,7 @@ export class PageEditor extends Component {
         }, 2000);
     }
 
-    async discardDraft() {
+    async discardDraft(notify = true) {
         let response;
         try {
             response = await window.$http.get(`/ajax/page/${this.pageId}`);
@@ -168,7 +175,7 @@ export class PageEditor extends Component {
         }
 
         this.draftDisplay.innerText = this.editingPageText;
-        this.toggleDiscardDraftVisibility(false);
+        this.discardDraftWrap.toggleAttribute('hidden', true);
         window.$events.emit('editor::replace', {
             html: response.data.html,
             markdown: response.data.markdown,
@@ -178,7 +185,30 @@ export class PageEditor extends Component {
         window.setTimeout(() => {
             this.startAutoSave();
         }, 1000);
-        window.$events.emit('success', this.draftDiscardedText);
+
+        if (notify) {
+            window.$events.success(this.draftDiscardedText);
+        }
+    }
+
+    async deleteDraft() {
+        /** @var {ConfirmDialog} * */
+        const dialog = window.$components.firstOnElement(this.deleteDraftDialogContainer, 'confirm-dialog');
+        const confirmed = await dialog.show();
+        if (!confirmed) {
+            return;
+        }
+
+        try {
+            const discard = this.discardDraft(false);
+            const draftDelete = window.$http.delete(`/page-revisions/user-drafts/${this.pageId}`);
+            await Promise.all([discard, draftDelete]);
+            window.$events.success(this.draftDeleteText);
+            this.deleteDraftWrap.toggleAttribute('hidden', true);
+        } catch (err) {
+            console.error(err);
+            window.$events.error(this.draftDeleteFailText);
+        }
     }
 
     updateChangelogDisplay() {
@@ -191,10 +221,6 @@ export class PageEditor extends Component {
         this.changelogDisplay.innerText = summary;
     }
 
-    toggleDiscardDraftVisibility(show) {
-        this.discardDraftWrap.classList.toggle('hidden', !show);
-    }
-
     async changeEditor(event) {
         event.preventDefault();
 
diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js
index 514bff87d..f66b7921d 100644
--- a/resources/js/markdown/actions.js
+++ b/resources/js/markdown/actions.js
@@ -433,7 +433,9 @@ export class Actions {
      */
     #setText(text, selectionRange = null) {
         selectionRange = selectionRange || this.#getSelectionRange();
-        this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from);
+        const newDoc = this.editor.cm.state.toText(text);
+        const newSelectFrom = Math.min(selectionRange.from, newDoc.length);
+        this.#dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
         this.focus();
     }
 
diff --git a/resources/views/pages/parts/editor-toolbar.blade.php b/resources/views/pages/parts/editor-toolbar.blade.php
index c29e6de0e..3b438de7c 100644
--- a/resources/views/pages/parts/editor-toolbar.blade.php
+++ b/resources/views/pages/parts/editor-toolbar.blade.php
@@ -27,13 +27,22 @@
                             </a>
                         </li>
                     @endif
-                    <li refs="page-editor@discardDraftWrap" class="{{ $isDraftRevision ? '' : 'hidden' }}">
-                        <button refs="page-editor@discardDraft" type="button" class="text-neg icon-item">
+                    <li refs="page-editor@discard-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
+                        <button refs="page-editor@discard-draft" type="button" class="text-warn icon-item">
                             @icon('cancel')
                             <div>{{ trans('entities.pages_edit_discard_draft') }}</div>
                         </button>
                     </li>
+                    <li refs="page-editor@delete-draft-wrap" {{ $isDraftRevision ? '' : 'hidden' }}>
+                        <button refs="page-editor@delete-draft" type="button" class="text-neg icon-item">
+                            @icon('delete')
+                            <div>{{ trans('entities.pages_edit_delete_draft') }}</div>
+                        </button>
+                    </li>
                     @if(userCan('editor-change'))
+                        <li>
+                            <hr>
+                        </li>
                         <li>
                             @if($editor === 'wysiwyg')
                                 <a href="{{ $model->getUrl($isDraft ? '' : '/edit') }}?editor=markdown-clean" refs="page-editor@changeEditor" class="icon-item">
diff --git a/resources/views/pages/parts/form.blade.php b/resources/views/pages/parts/form.blade.php
index a3a118527..4ed55044b 100644
--- a/resources/views/pages/parts/form.blade.php
+++ b/resources/views/pages/parts/form.blade.php
@@ -13,6 +13,8 @@
      option:page-editor:autosave-fail-text="{{ trans('errors.page_draft_autosave_fail') }}"
      option:page-editor:editing-page-text="{{ trans('entities.pages_editing_page') }}"
      option:page-editor:draft-discarded-text="{{ trans('entities.pages_draft_discarded') }}"
+     option:page-editor:draft-delete-text="{{ trans('entities.pages_draft_deleted') }}"
+     option:page-editor:draft-delete-fail-text="{{ trans('errors.page_draft_delete_fail') }}"
      option:page-editor:set-changelog-text="{{ trans('entities.pages_edit_set_changelog') }}">
 
     {{--Header Toolbar--}}
@@ -47,7 +49,7 @@
             class="text-link text-button hide-over-m page-save-mobile-button">@icon('save')</button>
 
     {{--Editor Change Dialog--}}
-    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switchDialog'])
+    @component('common.confirm-dialog', ['title' => trans('entities.pages_editor_switch_title'), 'ref' => 'page-editor@switch-dialog'])
         <p>
             {{ trans('entities.pages_editor_switch_are_you_sure') }}
             <br>
@@ -60,4 +62,11 @@
             <li>{{ trans('entities.pages_editor_switch_consideration_c') }}</li>
         </ul>
     @endcomponent
+
+    {{--Delete Draft Dialog--}}
+    @component('common.confirm-dialog', ['title' => trans('entities.pages_edit_delete_draft'), 'ref' => 'page-editor@delete-draft-dialog'])
+        <p>
+            {{ trans('entities.pages_edit_delete_draft_confirm') }}
+        </p>
+    @endcomponent
 </div>
\ No newline at end of file
diff --git a/routes/web.php b/routes/web.php
index 468c300ba..74ee74a2c 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -106,6 +106,7 @@ Route::middleware('auth')->group(function () {
     Route::get('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/changes', [EntityControllers\PageRevisionController::class, 'changes']);
     Route::put('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', [EntityControllers\PageRevisionController::class, 'restore']);
     Route::delete('/books/{bookSlug}/page/{pageSlug}/revisions/{revId}/delete', [EntityControllers\PageRevisionController::class, 'destroy']);
+    Route::delete('/page-revisions/user-drafts/{pageId}', [EntityControllers\PageRevisionController::class, 'destroyUserDraft']);
 
     // Chapters
     Route::get('/books/{bookSlug}/chapter/{chapterSlug}/create-page', [EntityControllers\PageController::class, 'create']);
diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php
index 75b1933ea..e99ba9b81 100644
--- a/tests/Entity/PageDraftTest.php
+++ b/tests/Entity/PageDraftTest.php
@@ -166,6 +166,30 @@ class PageDraftTest extends TestCase
         ]);
     }
 
+    public function test_user_draft_removed_on_user_drafts_delete_call()
+    {
+        $editor = $this->users->editor();
+        $page = $this->entities->page();
+
+        $this->actingAs($editor)->put('/ajax/page/' . $page->id . '/save-draft', [
+            'name' => $page->name,
+            'html' => '<p>updated draft again</p>',
+        ]);
+
+        $revisionData = [
+            'type' => 'update_draft',
+            'created_by' => $editor->id,
+            'page_id' => $page->id,
+        ];
+
+        $this->assertDatabaseHas('page_revisions', $revisionData);
+
+        $resp = $this->delete("/page-revisions/user-drafts/{$page->id}");
+
+        $resp->assertOk();
+        $this->assertDatabaseMissing('page_revisions', $revisionData);
+    }
+
     public function test_updating_page_draft_with_markdown_retains_markdown_content()
     {
         $book = $this->entities->book();

From f7ad387a10a898145845221ac3fd27df1f840c12 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Tue, 13 Jun 2023 15:52:33 +0100
Subject: [PATCH 3/5] CSS: Updated status colors to be CSS variables, Added
 dark variants

Needed some level of harcoding though due to callouts using colors,
which can't be css colors as DOMPDF won't understand these.
Use css variables elsewhere and added new dark variants to fit a bit
better.
---
 resources/sass/_blocks.scss     | 26 +++++++++++++-------------
 resources/sass/_codemirror.scss |  2 +-
 resources/sass/_colors.scss     | 12 ++++++------
 resources/sass/_components.scss | 14 +++++++-------
 resources/sass/_forms.scss      |  4 ++--
 resources/sass/_variables.scss  | 23 ++++++++++++++++++-----
 6 files changed, 47 insertions(+), 34 deletions(-)

diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss
index 1d9bfc272..a1268e6b4 100644
--- a/resources/sass/_blocks.scss
+++ b/resources/sass/_blocks.scss
@@ -3,7 +3,7 @@
  * Callouts
  */
 .callout {
-  border-inline-start: 3px solid #BBB;
+  border-left: 3px solid #BBB;
   background-color: #EEE;
   padding: $-s $-s $-s $-xl;
   display: block;
@@ -24,30 +24,30 @@
     opacity: 0.8;
   }
   &.success {
-    border-left-color: $positive;
-    @include lightDark(background-color, lighten($positive, 68%), darken($positive, 22%));
-    @include lightDark(color, darken($positive, 16%), lighten($positive, 5%));
+    @include lightDark(border-left-color, $positive, $positive-dark);
+    @include lightDark(background-color, lighten($positive, 68%), darken($positive-dark, 36%));
+    @include lightDark(color, darken($positive, 16%), $positive-dark);
   }
   &.success:before {
     background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
   }
   &.danger {
-    border-left-color: $negative;
-    @include lightDark(background-color, lighten($negative, 56%), darken($negative, 30%));
-    @include lightDark(color, darken($negative, 20%), lighten($negative, 5%));
+    @include lightDark(border-left-color, $negative, $negative-dark);
+    @include lightDark(background-color, lighten($negative, 56%), darken($negative-dark, 55%));
+    @include lightDark(color, darken($negative, 20%), $negative-dark);
   }
   &.danger:before {
     background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
   }
   &.info {
-    border-left-color: $info;
-    @include lightDark(color, darken($info, 20%), lighten($info, 10%));
-    @include lightDark(background-color, lighten($info, 50%), darken($info, 35%));
+    @include lightDark(border-left-color, $info, $info-dark);
+    @include lightDark(color, darken($info, 20%), $info-dark);
+    @include lightDark(background-color, lighten($info, 50%), darken($info-dark, 34%));
   }
   &.warning {
-    border-left-color: $warning;
-    @include lightDark(background-color, lighten($warning, 50%), darken($warning, 36%));
-    @include lightDark(color, darken($warning, 20%), $warning);
+    @include lightDark(border-left-color, $warning, $warning-dark);
+    @include lightDark(background-color, lighten($warning, 50%), darken($warning-dark, 50%));
+    @include lightDark(color, darken($warning, 20%), $warning-dark);
   }
   &.warning:before {
     background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
diff --git a/resources/sass/_codemirror.scss b/resources/sass/_codemirror.scss
index 0fd347cf8..50f5bdc10 100644
--- a/resources/sass/_codemirror.scss
+++ b/resources/sass/_codemirror.scss
@@ -50,7 +50,7 @@
     fill: currentColor;
   }
   &.success {
-    background: $positive;
+    background: var(--color-positive);
     color: #FFF;
   }
   &:focus {
diff --git a/resources/sass/_colors.scss b/resources/sass/_colors.scss
index aff9ff6d0..c77c1d8b3 100644
--- a/resources/sass/_colors.scss
+++ b/resources/sass/_colors.scss
@@ -22,18 +22,18 @@
  * Status text colors
  */
 .text-pos, .text-pos:hover, .text-pos-hover:hover {
-  color: $positive !important;
-  fill: $positive !important;
+  color: var(--color-positive) !important;
+  fill: var(--color-positive) !important;
 }
 
 .text-warn, .text-warn:hover, .text-warn-hover:hover {
-  color: $warning !important;
-  fill: $warning !important;
+  color: var(--color-warning) !important;
+  fill: var(--color-warning) !important;
 }
 
 .text-neg, .text-neg:hover, .text-neg-hover:hover  {
-  color: $negative !important;
-  fill: $negative !important;
+  color: var(--color-negative) !important;
+  fill: var(--color-negative) !important;
 }
 
 /*
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 321c26e88..54c9434c4 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -46,13 +46,13 @@
     }
   }
   &.pos {
-    color: $positive;
+    color: var(--color-positive);
   }
   &.neg {
-    color: $negative;
+    color: var(--color-negative);
   }
   &.warning {
-    color: $warning;
+    color: var(--color-warning);
   }
   &.showing {
     transform: translateX(0);
@@ -334,10 +334,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   line-height: 1.2;
 }
 .dropzone-file-item-status[data-status="success"] {
-  color: $positive;
+  color: var(--color-positive);
 }
 .dropzone-file-item-status[data-status="error"] {
-  color: $negative;
+  color: var(--color-negative);
 }
 .dropzone-file-item-status[data-status] + .dropzone-file-item-label {
   display: none;
@@ -921,10 +921,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   display: inline-block;
 }
 .status-indicator-active {
-  background-color: $positive;
+  background-color: var(--color-positive);
 }
 .status-indicator-inactive {
-  background-color: $negative;
+  background-color: var(--color-negative);
 }
 
 .shortcut-container {
diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss
index 5276bb566..4722d9aa1 100644
--- a/resources/sass/_forms.scss
+++ b/resources/sass/_forms.scss
@@ -13,10 +13,10 @@
   max-width: 100%;
 
   &.neg, &.invalid {
-    border: 1px solid $negative;
+    border: 1px solid var(--color-negative);
   }
   &.pos, &.valid {
-    border: 1px solid $positive;
+    border: 1px solid var(--color-positive);
   }
   &.disabled, &[disabled] {
     background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss
index aac9223f9..10329c700 100644
--- a/resources/sass/_variables.scss
+++ b/resources/sass/_variables.scss
@@ -36,6 +36,15 @@ $fs-m: 14px;
 $fs-s: 12px;
 
 // Colours
+$positive: #0f7d15;
+$negative: #ab0f0e;
+$info: #0288D1;
+$warning: #cf4d03;
+$positive-dark: #4aa850;
+$negative-dark: #e85c5b;
+$info-dark: #0288D1;
+$warning-dark: #de8a5a;
+
 :root {
   --color-primary: #206ea7;
   --color-primary-light: rgba(32,110,167,0.15);
@@ -47,22 +56,26 @@ $fs-s: 12px;
   --color-book: #077b70;
   --color-bookshelf: #a94747;
 
+  --color-positive: #{$positive};
+  --color-negative: #{$negative};
+  --color-info: #{$info};
+  --color-warning: #{$warning};
+
   --bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");
 }
 
 :root.dark-mode {
   --bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");
   color-scheme: only dark;
+
+  --color-positive: #4aa850;
+  --color-negative: #e85c5b;
+  --color-warning: #de8a5a;
 }
 :root:not(.dark-mode) {
   color-scheme: only light;
 }
 
-$positive: #0f7d15;
-$negative: #ab0f0e;
-$info: #0288D1;
-$warning: #cf4d03;
-
 // Text colours
 $text-dark: #444;
 

From 610ad0d613513d8f83eb05a0ba099700846af7c6 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Wed, 14 Jun 2023 12:53:48 +0100
Subject: [PATCH 4/5] Updated fonts to be defined via CSS variables

Exports system remains separate due to lacking css variable support.
---
 resources/sass/_codemirror.scss |  4 ++++
 resources/sass/_components.scss |  2 +-
 resources/sass/_text.scss       |  9 +++++----
 resources/sass/_tinymce.scss    |  2 +-
 resources/sass/_variables.scss  | 36 +++++++++++++++++++--------------
 5 files changed, 32 insertions(+), 21 deletions(-)

diff --git a/resources/sass/_codemirror.scss b/resources/sass/_codemirror.scss
index 50f5bdc10..c4b0e2e89 100644
--- a/resources/sass/_codemirror.scss
+++ b/resources/sass/_codemirror.scss
@@ -14,6 +14,10 @@
   border-radius: 4px;
 }
 
+.cm-editor .cm-line, .cm-editor .cm-gutter {
+  font-family: var(--font-code);
+}
+
 // Manual dark-mode definition so that it applies to code blocks within the shadow
 // dom which are used within the WYSIWYG editor, as the .dark-mode on the parent
 // <html> node are not applies so instead we have the class on the parent element.
diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss
index 54c9434c4..dab74341a 100644
--- a/resources/sass/_components.scss
+++ b/resources/sass/_components.scss
@@ -574,7 +574,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   cursor: pointer;
   width: 100%;
   text-align: left;
-  font-family: $mono;
+  font-family: var(--font-code);
   font-size: 0.7rem;
   padding-left: 24px + $-xs;
   &:hover, &.active {
diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss
index adfc87ad1..b00f51cd7 100644
--- a/resources/sass/_text.scss
+++ b/resources/sass/_text.scss
@@ -3,10 +3,10 @@
  */
 
 body, button, input, select, label, textarea {
-  font-family: $text;
+  font-family: var(--font-body);
 }
-.Codemirror, pre, #markdown-editor-input, .text-mono, .code-base {
-  font-family: $mono;
+pre, #markdown-editor-input, .text-mono, .code-base {
+  font-family: var(--font-code);
 }
 
 /*
@@ -42,6 +42,7 @@ h1, h2, h3, h4, h5, h6 {
   font-weight: 400;
   position: relative;
   display: block;
+  font-family: var(--font-heading);
   @include lightDark(color, #222, #BBB);
   .subheader {
     font-size: 0.5em;
@@ -226,7 +227,7 @@ blockquote {
 }
 
 .text-mono {
-  font-family: $mono;
+  font-family: var(--font-code);
 }
 
 .text-uppercase {
diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss
index 7170f8101..13b6f676b 100644
--- a/resources/sass/_tinymce.scss
+++ b/resources/sass/_tinymce.scss
@@ -110,7 +110,7 @@ body.page-content.mce-content-body  {
   border-left: 3px solid currentColor !important;
 }
 .tox-menu .tox-collection__item[title^="<"] > div > div {
-  font-family: $mono !important;
+  font-family: var(--font-code) !important;
   border: 1px solid #DDD !important;
   background-color: #EEE !important;
   padding: 4px 6px !important;
diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss
index 10329c700..5892237d9 100644
--- a/resources/sass/_variables.scss
+++ b/resources/sass/_variables.scss
@@ -27,11 +27,11 @@ $-xxs: 3px;
 $spacing: (('none', 0), ('xxs', $-xxs), ('xs', $-xs), ('s', $-s), ('m', $-m), ('l', $-l), ('xl', $-xl), ('xxl', $-xxl), ('auto', auto));
 
 // Fonts
-$text: -apple-system, BlinkMacSystemFont,
+$font-body: -apple-system, BlinkMacSystemFont,
 "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell",
 "Fira Sans", "Droid Sans", "Helvetica Neue",
 sans-serif;
-$mono: "Lucida Console", "DejaVu Sans Mono", "Ubuntu Mono", Monaco, monospace;
+$font-mono: "Lucida Console", "DejaVu Sans Mono", "Ubuntu Mono", Monaco, monospace;
 $fs-m: 14px;
 $fs-s: 12px;
 
@@ -45,7 +45,25 @@ $negative-dark: #e85c5b;
 $info-dark: #0288D1;
 $warning-dark: #de8a5a;
 
+// Text colours
+$text-dark: #444;
+
+// Shadows
+$bs-light: 0 0 4px 1px #CCC;
+$bs-dark: 0 0 4px 1px rgba(0, 0, 0, 0.5);
+$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
+$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);
+$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
+$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);
+$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
+
+// CSS root variables
 :root {
+  --font-body: #{$font-body};
+  --font-heading: #{$font-body};
+  --font-code: #{$font-mono};
+
+
   --color-primary: #206ea7;
   --color-primary-light: rgba(32,110,167,0.15);
   --color-link: #206ea7;
@@ -74,16 +92,4 @@ $warning-dark: #de8a5a;
 }
 :root:not(.dark-mode) {
   color-scheme: only light;
-}
-
-// Text colours
-$text-dark: #444;
-
-// Shadows
-$bs-light: 0 0 4px 1px #CCC;
-$bs-dark: 0 0 4px 1px rgba(0, 0, 0, 0.5);
-$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
-$bs-large: 0 1px 6px 1px rgba(22, 22, 22, 0.2);
-$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1);
-$bs-card-dark: 0 1px 6px -1px rgba(0, 0, 0, 0.5);
-$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
+}
\ No newline at end of file

From 70be2e8c9eb6872c9bf06a762f024bd8c6a306da Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Wed, 14 Jun 2023 13:18:14 +0100
Subject: [PATCH 5/5] CSS: Reduced styles used in export formats

Extracted many main page content styles to own scss partial.
Styles could do with a more general clean-up.

Closes #4303
---
 resources/sass/_blocks.scss       |  60 ----------
 resources/sass/_content.scss      | 175 ++++++++++++++++++++++++++++++
 resources/sass/_pages.scss        | 112 -------------------
 resources/sass/_text.scss         |   3 +-
 resources/sass/export-styles.scss |   5 +-
 resources/sass/styles.scss        |   1 +
 6 files changed, 179 insertions(+), 177 deletions(-)
 create mode 100644 resources/sass/_content.scss

diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss
index a1268e6b4..54c509ef9 100644
--- a/resources/sass/_blocks.scss
+++ b/resources/sass/_blocks.scss
@@ -1,63 +1,3 @@
-
-/**
- * Callouts
- */
-.callout {
-  border-left: 3px solid #BBB;
-  background-color: #EEE;
-  padding: $-s $-s $-s $-xl;
-  display: block;
-  position: relative;
-  overflow: auto;
-  &:before {
-    background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');
-    background-repeat: no-repeat;
-    content: '';
-    width: 1.2em;
-    height: 1.2em;
-    left: $-xs + 2px;
-    top: 50%;
-    margin-top: -9px;
-    display: inline-block;
-    position: absolute;
-    line-height: 1;
-    opacity: 0.8;
-  }
-  &.success {
-    @include lightDark(border-left-color, $positive, $positive-dark);
-    @include lightDark(background-color, lighten($positive, 68%), darken($positive-dark, 36%));
-    @include lightDark(color, darken($positive, 16%), $positive-dark);
-  }
-  &.success:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
-  }
-  &.danger {
-    @include lightDark(border-left-color, $negative, $negative-dark);
-    @include lightDark(background-color, lighten($negative, 56%), darken($negative-dark, 55%));
-    @include lightDark(color, darken($negative, 20%), $negative-dark);
-  }
-  &.danger:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
-  }
-  &.info {
-    @include lightDark(border-left-color, $info, $info-dark);
-    @include lightDark(color, darken($info, 20%), $info-dark);
-    @include lightDark(background-color, lighten($info, 50%), darken($info-dark, 34%));
-  }
-  &.warning {
-    @include lightDark(border-left-color, $warning, $warning-dark);
-    @include lightDark(background-color, lighten($warning, 50%), darken($warning-dark, 50%));
-    @include lightDark(color, darken($warning, 20%), $warning-dark);
-  }
-  &.warning:before {
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
-  }
-  a {
-    color: inherit;
-    text-decoration: underline;
-  }
-}
-
 /**
  * Card-style blocks
  */
diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss
new file mode 100644
index 000000000..10a2cd983
--- /dev/null
+++ b/resources/sass/_content.scss
@@ -0,0 +1,175 @@
+/**
+ * Page Content
+ * Styles specific to blocks used within page content.
+ */
+
+.page-content {
+  width: 100%;
+  max-width: 840px;
+  margin: 0 auto;
+  overflow-wrap: break-word;
+  .align-left {
+    text-align: left;
+  }
+  img.align-left, table.align-left {
+    float: left !important;
+    margin: $-xs $-m $-m 0;
+  }
+  .align-right {
+    text-align: right !important;
+  }
+  img.align-right, table.align-right {
+    float: right !important;
+    margin: $-xs 0 $-xs $-s;
+  }
+  .align-center {
+    text-align: center;
+  }
+  img.align-center {
+    display: block;
+  }
+  img.align-center, table.align-center {
+    margin-left: auto;
+    margin-right: auto;
+  }
+  img {
+    max-width: 100%;
+    height:auto;
+  }
+  h1, h2, h3, h4, h5, h6, pre {
+    clear: left;
+  }
+  hr {
+    clear: both;
+    margin: $-m 0;
+  }
+  table {
+    hyphens: auto;
+    table-layout: fixed;
+    max-width: 100%;
+    height: auto !important;
+  }
+
+  // diffs
+  ins,
+  del {
+    text-decoration: none;
+  }
+  ins {
+    background: #dbffdb;
+  }
+  del {
+    background: #FFECEC;
+  }
+
+  details {
+    border: 1px solid;
+    @include lightDark(border-color, #DDD, #555);
+    margin-bottom: 1em;
+    padding: $-s;
+  }
+  details > summary {
+    margin-top: -$-s;
+    margin-left: -$-s;
+    margin-right: -$-s;
+    margin-bottom: -$-s;
+    font-weight: bold;
+    @include lightDark(background-color, #EEE, #333);
+    padding: $-xs $-s;
+  }
+  details[open] > summary {
+    margin-bottom: $-s;
+    border-bottom: 1px solid;
+    @include lightDark(border-color, #DDD, #555);
+  }
+  details > summary + * {
+    margin-top: .2em;
+  }
+  details:after {
+    content: '';
+    display: block;
+    clear: both;
+  }
+
+  li > input[type="checkbox"] {
+    vertical-align: top;
+    margin-top: 0.3em;
+  }
+
+  p:empty {
+    min-height: 1.6em;
+  }
+
+  &.page-revision {
+    pre code {
+      white-space: pre-wrap;
+    }
+  }
+
+  .cm-editor {
+    margin-bottom: 1.375em;
+  }
+
+  video {
+    max-width: 100%;
+  }
+}
+
+/**
+ * Callouts
+ */
+.callout {
+  border-left: 3px solid #BBB;
+  background-color: #EEE;
+  padding: $-s $-s $-s $-xl;
+  display: block;
+  position: relative;
+  overflow: auto;
+  &:before {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');
+    background-repeat: no-repeat;
+    content: '';
+    width: 1.2em;
+    height: 1.2em;
+    left: $-xs + 2px;
+    top: 50%;
+    margin-top: -9px;
+    display: inline-block;
+    position: absolute;
+    line-height: 1;
+    opacity: 0.8;
+  }
+  &.success {
+    @include lightDark(border-left-color, $positive, $positive-dark);
+    @include lightDark(background-color, lighten($positive, 68%), darken($positive-dark, 36%));
+    @include lightDark(color, darken($positive, 16%), $positive-dark);
+  }
+  &.success:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
+  }
+  &.danger {
+    @include lightDark(border-left-color, $negative, $negative-dark);
+    @include lightDark(background-color, lighten($negative, 56%), darken($negative-dark, 55%));
+    @include lightDark(color, darken($negative, 20%), $negative-dark);
+  }
+  &.danger:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
+  }
+  &.info {
+    @include lightDark(border-left-color, $info, $info-dark);
+    @include lightDark(color, darken($info, 20%), $info-dark);
+    @include lightDark(background-color, lighten($info, 50%), darken($info-dark, 34%));
+  }
+  &.warning {
+    @include lightDark(border-left-color, $warning, $warning-dark);
+    @include lightDark(background-color, lighten($warning, 50%), darken($warning-dark, 50%));
+    @include lightDark(color, darken($warning, 20%), $warning-dark);
+  }
+  &.warning:before {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
+  }
+  a {
+    color: inherit;
+    text-decoration: underline;
+  }
+}
\ No newline at end of file
diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss
index 2a77e84ba..fbac1de07 100755
--- a/resources/sass/_pages.scss
+++ b/resources/sass/_pages.scss
@@ -76,118 +76,6 @@ body.tox-fullscreen, body.markdown-fullscreen {
   padding: 0 !important;
 }
 
-.page-content {
-  width: 100%;
-  max-width: 840px;
-  margin: 0 auto;
-  overflow-wrap: break-word;
-  .align-left {
-    text-align: left;
-  }
-  img.align-left, table.align-left {
-    float: left !important;
-    margin: $-xs $-m $-m 0;
-  }
-  .align-right {
-    text-align: right !important;
-  }
-  img.align-right, table.align-right {
-    float: right !important;
-    margin: $-xs 0 $-xs $-s;
-  }
-  .align-center {
-    text-align: center;
-  }
-  img.align-center {
-    display: block;
-  }
-  img.align-center, table.align-center {
-    margin-left: auto;
-    margin-right: auto;
-  }
-  img {
-    max-width: 100%;
-    height:auto;
-  }
-  h1, h2, h3, h4, h5, h6, pre {
-    clear: left;
-  }
-  hr {
-    clear: both;
-    margin: $-m 0;
-  }
-  table {
-    hyphens: auto;
-    table-layout: fixed;
-    max-width: 100%;
-    height: auto !important;
-  }
-
-  // diffs
-  ins,
-  del {
-    text-decoration: none;
-  }
-  ins {
-    background: #dbffdb;
-  }
-  del {
-    background: #FFECEC;
-  }
-
-  details {
-    border: 1px solid;
-    @include lightDark(border-color, #DDD, #555);
-    margin-bottom: 1em;
-    padding: $-s;
-  }
-  details > summary {
-    margin-top: -$-s;
-    margin-left: -$-s;
-    margin-right: -$-s;
-    margin-bottom: -$-s;
-    font-weight: bold;
-    @include lightDark(background-color, #EEE, #333);
-    padding: $-xs $-s;
-  }
-  details[open] > summary {
-    margin-bottom: $-s;
-    border-bottom: 1px solid;
-    @include lightDark(border-color, #DDD, #555);
-  }
-  details > summary + * {
-    margin-top: .2em;
-  }
-  details:after {
-    content: '';
-    display: block;
-    clear: both;
-  }
-
-  li > input[type="checkbox"] {
-    vertical-align: top;
-    margin-top: 0.3em;
-  }
-
-  p:empty {
-    min-height: 1.6em;
-  }
-
-  &.page-revision {
-    pre code {
-      white-space: pre-wrap;
-    }
-  }
-
-  .cm-editor {
-    margin-bottom: 1.375em;
-  }
-
-  video {
-    max-width: 100%;
-  }
-}
-
 // Page content pointers
 .pointer-container {
   position: fixed;
diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss
index b00f51cd7..f2c88d96d 100644
--- a/resources/sass/_text.scss
+++ b/resources/sass/_text.scss
@@ -211,7 +211,8 @@ pre {
 blockquote {
   display: block;
   position: relative;
-  border-left: 4px solid var(--color-primary);
+  border-left: 4px solid transparent;
+  border-left-color: var(--color-primary);
   @include lightDark(background-color, #f8f8f8, #333);
   padding: $-s $-m $-s $-xl;
   overflow: auto;
diff --git a/resources/sass/export-styles.scss b/resources/sass/export-styles.scss
index 1e39bd056..cfa1ebdf8 100644
--- a/resources/sass/export-styles.scss
+++ b/resources/sass/export-styles.scss
@@ -3,11 +3,8 @@
 @import "mixins";
 @import "html";
 @import "text";
-@import "layout";
-@import "blocks";
 @import "tables";
-@import "lists";
-@import "pages";
+@import "content";
 
 html, body {
   background-color: #FFF;
diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss
index 9a8e5b36d..c0ce7ba63 100644
--- a/resources/sass/styles.scss
+++ b/resources/sass/styles.scss
@@ -21,6 +21,7 @@
 @import "footer";
 @import "lists";
 @import "pages";
+@import "content";
 
 // Jquery Sortable Styles
 .dragged {