From 7101ce3050752f679f2be8bc177c29b1abc00715 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 Sep 2022 16:40:42 +0100 Subject: [PATCH] Added "page_include_parse" theme event For custom control of include tag parsing. --- app/Entities/Tools/PageContent.php | 33 +++++++++++++++++++----------- app/Theming/ThemeEvents.php | 20 ++++++++++++++++-- tests/ThemeTest.php | 30 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index ea6a185f1..17cd4a0ad 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -5,6 +5,8 @@ namespace BookStack\Entities\Tools; use BookStack\Entities\Models\Page; use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Exceptions\ImageUploadException; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageService; use BookStack\Util\HtmlContentFilter; @@ -372,23 +374,30 @@ class PageContent continue; } - // Find page and skip this if page not found + // Find page to use, and default replacement to empty string for non-matches. /** @var ?Page $matchedPage */ $matchedPage = Page::visible()->find($pageId); - if ($matchedPage === null) { - $html = str_replace($fullMatch, '', $html); - continue; + $replacement = ''; + + if ($matchedPage && count($splitInclude) === 1) { + // If we only have page id, just insert all page html and continue. + $replacement = $matchedPage->html; + } else if ($matchedPage && count($splitInclude) > 1) { + // Otherwise, if our include tag defines a section, load that specific content + $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]); + $replacement = trim($innerContent); } - // If we only have page id, just insert all page html and continue. - if (count($splitInclude) === 1) { - $html = str_replace($fullMatch, $matchedPage->html, $html); - continue; - } + $themeReplacement = Theme::dispatch( + ThemeEvents::PAGE_INCLUDE_PARSE, + $includeId, + $replacement, + clone $this->page, + $matchedPage ? (clone $matchedPage) : null, + ); - // Create and load HTML into a document - $innerContent = $this->fetchSectionOfPage($matchedPage, $splitInclude[1]); - $html = str_replace($fullMatch, trim($innerContent), $html); + // Perform the content replacement + $html = str_replace($fullMatch, $themeReplacement ?? $replacement, $html); } return $html; diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 427147146..0a8efaee4 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -2,6 +2,8 @@ namespace BookStack\Theming; +use BookStack\Entities\Models\Page; + /** * The ThemeEvents used within BookStack. * @@ -60,8 +62,7 @@ class ThemeEvents /** * Commonmark environment configure. - * Provides the commonmark library environment for customization - * before it's used to render markdown content. + * Provides the commonmark library environment for customization before it's used to render markdown content. * If the listener returns a non-null value, that will be used as an environment instead. * * @param \League\CommonMark\ConfigurableEnvironmentInterface $environment @@ -69,6 +70,21 @@ class ThemeEvents */ const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure'; + /** + * Page include parse event. + * Runs when a page include tag is being parsed, typically when page content is being processed for viewing. + * Provides the "include tag" reference string, the default BookStack replacement content for the tag, + * the current page being processed, and the page that's being referenced by the include tag. + * The referenced page may be null where the page does not exist or where permissions prevent visibility. + * If the listener returns a non-null value, that will be used as the replacement HTML content instead. + * + * @param string $tagReference + * @param string $replacementHTML + * @param Page $currentPage + * @param ?Page $referencedPage + */ + const PAGE_INCLUDE_PARSE = 'page_include_parse'; + /** * Web before middleware action. * Runs before the request is handled but after all other middleware apart from those diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index c90625e2b..689c27488 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -214,6 +214,36 @@ class ThemeTest extends TestCase $this->assertEquals($book->id, $args[1]->id); } + public function test_event_page_include_parse() + { + /** @var Page $page */ + /** @var Page $otherPage */ + $page = Page::query()->first(); + $otherPage = Page::query()->where('id', '!=', $page->id)->first(); + $otherPage->html = '

This is a really cool section

'; + $page->html = "

{{@{$otherPage->id}#bkmrk-cool}}

"; + $page->save(); + $otherPage->save(); + + $args = []; + $callback = function (...$eventArgs) use (&$args) { + $args = $eventArgs; + return 'Big & content replace surprise!'; + }; + + Theme::listen(ThemeEvents::PAGE_INCLUDE_PARSE, $callback); + $resp = $this->asEditor()->get($page->getUrl()); + $this->withHtml($resp)->assertElementContains('.page-content strong', 'Big & content replace surprise!'); + + $this->assertCount(4, $args); + $this->assertEquals($otherPage->id . '#bkmrk-cool', $args[0]); + $this->assertEquals('This is a really cool section', $args[1]); + $this->assertTrue($args[2] instanceof Page); + $this->assertTrue($args[3] instanceof Page); + $this->assertEquals($page->id, $args[2]->id); + $this->assertEquals($otherPage->id, $args[3]->id); + } + public function test_add_social_driver() { Theme::addSocialDriver('catnet', [