Theme System: Added new page-content focused events

Closes #6049
This commit is contained in:
Dan Brown
2026-03-06 12:40:22 +00:00
parent d0d1bb9829
commit 27240be499
3 changed files with 127 additions and 6 deletions

View File

@@ -39,7 +39,14 @@ class PageContent
public function setNewHTML(string $html, User $updater): void
{
$html = $this->extractBase64ImagesFromHtml($html, $updater);
$this->page->html = $this->formatHtml($html);
$html = $this->formatHtml($html);
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);
if (is_string($themeResult)) {
$html = $themeResult;
}
$this->page->html = $html;
$this->page->text = $this->toPlainText();
$this->page->markdown = '';
}
@@ -52,7 +59,14 @@ class PageContent
$markdown = $this->extractBase64ImagesFromMarkdown($markdown, $updater);
$this->page->markdown = $markdown;
$html = (new MarkdownToHtml($markdown))->convert();
$this->page->html = $this->formatHtml($html);
$html = $this->formatHtml($html);
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);
if (is_string($themeResult)) {
$html = $themeResult;
}
$this->page->html = $html;
$this->page->text = $this->toPlainText();
}
@@ -81,7 +95,7 @@ class PageContent
/**
* Convert all inline base64 content to uploaded image files.
* Regex is used to locate the start of data-uri definitions then
* Regex is used to locate the start of data-uri definitions, then
* manual looping over content is done to parse the whole data uri.
* Attempting to capture the whole data uri using regex can cause PHP
* PCRE limits to be hit with larger, multi-MB, files.
@@ -301,7 +315,7 @@ class PageContent
$html = $this->page->html ?? '';
if (empty($html)) {
return $html;
return $this->handlePostRender('');
}
$doc = new HtmlDocument($html);
@@ -322,7 +336,7 @@ class PageContent
$cacheKey = $this->getContentCacheKey($doc->getBodyInnerHtml());
$cached = cache()->get($cacheKey, null);
if ($cached !== null) {
return $cached;
return $this->handlePostRender($cached);
}
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
@@ -332,7 +346,13 @@ class PageContent
$cacheTime = 86400 * 7; // 1 week
cache()->put($cacheKey, $filtered, $cacheTime);
return $filtered;
return $this->handlePostRender($filtered);
}
protected function handlePostRender(string $html): string
{
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_POST_RENDER, $html, $this->page);
return is_string($themeResult) ? $themeResult : $html;
}
protected function getContentCacheKey(string $html): string

View File

@@ -111,6 +111,31 @@ class ThemeEvents
*/
const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';
/**
* Page content post-render event.
* Runs after any display rendering of page content, typically when page content is being processed for viewing.
* Rendering typically includes parsing of page includes, and content filtering.
* Provides the HTML content about to be shown, along with the related page instance.
* If the listener returns a string value, that will be used as the HTML content instead.
*
* @param string $html
* @param \BookStack\Entities\Models\Page $page
* @return string|null
*/
const PAGE_CONTENT_POST_RENDER = 'page_content_post_render';
/**
* Page content pre-store event.
* Runs just before page HTML is stored in the database, after BookStack's own processing.
* Provides the HTML content about to be stored, along with the related page instance.
* If the listener returns a string value, that will be used as the HTML content instead.
*
* @param string $html
* @param \BookStack\Entities\Models\Page $page
* @return string|null
*/
const PAGE_CONTENT_PRE_STORE = 'page_content_pre_store';
/**
* Page include parse event.
* Runs when a page include tag is being parsed, typically when page content is being processed for viewing.

View File

@@ -215,6 +215,82 @@ class LogicalThemeEventsTest extends TestCase
$this->assertEquals($book->id, $args[1]->id);
}
public function test_page_content_pre_store_fires_on_page_save()
{
$page = $this->entities->page();
$args = [];
$callback = function (...$eventArgs) use (&$args) {
$args = $eventArgs;
return '<p>New Content!</p>';
};
Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, $callback);
$this->asEditor();
$this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);
$this->assertCount(2, $args);
$this->assertEquals($page->id, $args[1]->id);
$this->assertEquals('<p id="bkmrk-old-content%21">Old content!</p>', $args[0]);
$newPageHtml = $page->refresh()->html;
$this->assertEquals('<p>New Content!</p>', $newPageHtml);
}
public function test_page_content_pre_store_does_not_change_content_if_nothing_returned()
{
$page = $this->entities->page();
Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, fn() => null);
$this->asEditor();
$this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);
$newPageHtml = $page->refresh()->html;
$this->assertEquals('<p id="bkmrk-old-content%21">Old content!</p>', $newPageHtml);
}
public function test_page_content_post_render_fires_on_page_view()
{
$page = $this->entities->page();
$page->html = '<p>Old content!</p>';
$page->save();
$args = [];
$callback = function (...$eventArgs) use (&$args) {
$args = $eventArgs;
return '<p>New postrendercontentforyou!</p>';
};
Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);
$resp = $this->asEditor()->get($page->getUrl());
$resp->assertSee('<p>New postrendercontentforyou!</p>', false);
$this->assertCount(2, $args);
$this->assertEquals($page->id, $args[1]->id);
$this->assertEquals('<p>Old content!</p>', $args[0]);
}
public function test_page_content_post_render_returns_original_content_if_no_return()
{
$page = $this->entities->page();
$page->html = '<p>Old content!</p>';
$page->save();
$args = [];
$callback = function (...$eventArgs) use (&$args) {
$args = $eventArgs;
};
Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);
$resp = $this->asEditor()->get($page->getUrl());
$resp->assertSee('<p>Old content!</p>', false);
$this->assertCount(2, $args);
}
public function test_page_include_parse()
{
/** @var Page $page */