Skip to content

Commit 27240be

Browse files
committed
Theme System: Added new page-content focused events
Closes #6049
1 parent d0d1bb9 commit 27240be

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

app/Entities/Tools/PageContent.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,14 @@ public function __construct(
3939
public function setNewHTML(string $html, User $updater): void
4040
{
4141
$html = $this->extractBase64ImagesFromHtml($html, $updater);
42-
$this->page->html = $this->formatHtml($html);
42+
$html = $this->formatHtml($html);
43+
44+
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);
45+
if (is_string($themeResult)) {
46+
$html = $themeResult;
47+
}
48+
49+
$this->page->html = $html;
4350
$this->page->text = $this->toPlainText();
4451
$this->page->markdown = '';
4552
}
@@ -52,7 +59,14 @@ public function setNewMarkdown(string $markdown, User $updater): void
5259
$markdown = $this->extractBase64ImagesFromMarkdown($markdown, $updater);
5360
$this->page->markdown = $markdown;
5461
$html = (new MarkdownToHtml($markdown))->convert();
55-
$this->page->html = $this->formatHtml($html);
62+
$html = $this->formatHtml($html);
63+
64+
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_PRE_STORE, $html, $this->page);
65+
if (is_string($themeResult)) {
66+
$html = $themeResult;
67+
}
68+
69+
$this->page->html = $html;
5670
$this->page->text = $this->toPlainText();
5771
}
5872

@@ -81,7 +95,7 @@ protected function extractBase64ImagesFromHtml(string $htmlText, User $updater):
8195

8296
/**
8397
* Convert all inline base64 content to uploaded image files.
84-
* Regex is used to locate the start of data-uri definitions then
98+
* Regex is used to locate the start of data-uri definitions, then
8599
* manual looping over content is done to parse the whole data uri.
86100
* Attempting to capture the whole data uri using regex can cause PHP
87101
* PCRE limits to be hit with larger, multi-MB, files.
@@ -301,7 +315,7 @@ public function render(bool $blankIncludes = false): string
301315
$html = $this->page->html ?? '';
302316

303317
if (empty($html)) {
304-
return $html;
318+
return $this->handlePostRender('');
305319
}
306320

307321
$doc = new HtmlDocument($html);
@@ -322,7 +336,7 @@ public function render(bool $blankIncludes = false): string
322336
$cacheKey = $this->getContentCacheKey($doc->getBodyInnerHtml());
323337
$cached = cache()->get($cacheKey, null);
324338
if ($cached !== null) {
325-
return $cached;
339+
return $this->handlePostRender($cached);
326340
}
327341

328342
$filterConfig = HtmlContentFilterConfig::fromConfigString(config('app.content_filtering'));
@@ -332,7 +346,13 @@ public function render(bool $blankIncludes = false): string
332346
$cacheTime = 86400 * 7; // 1 week
333347
cache()->put($cacheKey, $filtered, $cacheTime);
334348

335-
return $filtered;
349+
return $this->handlePostRender($filtered);
350+
}
351+
352+
protected function handlePostRender(string $html): string
353+
{
354+
$themeResult = Theme::dispatch(ThemeEvents::PAGE_CONTENT_POST_RENDER, $html, $this->page);
355+
return is_string($themeResult) ? $themeResult : $html;
336356
}
337357

338358
protected function getContentCacheKey(string $html): string

app/Theming/ThemeEvents.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@ class ThemeEvents
111111
*/
112112
const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';
113113

114+
/**
115+
* Page content post-render event.
116+
* Runs after any display rendering of page content, typically when page content is being processed for viewing.
117+
* Rendering typically includes parsing of page includes, and content filtering.
118+
* Provides the HTML content about to be shown, along with the related page instance.
119+
* If the listener returns a string value, that will be used as the HTML content instead.
120+
*
121+
* @param string $html
122+
* @param \BookStack\Entities\Models\Page $page
123+
* @return string|null
124+
*/
125+
const PAGE_CONTENT_POST_RENDER = 'page_content_post_render';
126+
127+
/**
128+
* Page content pre-store event.
129+
* Runs just before page HTML is stored in the database, after BookStack's own processing.
130+
* Provides the HTML content about to be stored, along with the related page instance.
131+
* If the listener returns a string value, that will be used as the HTML content instead.
132+
*
133+
* @param string $html
134+
* @param \BookStack\Entities\Models\Page $page
135+
* @return string|null
136+
*/
137+
const PAGE_CONTENT_PRE_STORE = 'page_content_pre_store';
138+
114139
/**
115140
* Page include parse event.
116141
* Runs when a page include tag is being parsed, typically when page content is being processed for viewing.

tests/Theme/LogicalThemeEventsTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,82 @@ public function test_activity_logged()
215215
$this->assertEquals($book->id, $args[1]->id);
216216
}
217217

218+
public function test_page_content_pre_store_fires_on_page_save()
219+
{
220+
$page = $this->entities->page();
221+
222+
$args = [];
223+
$callback = function (...$eventArgs) use (&$args) {
224+
$args = $eventArgs;
225+
return '<p>New Content!</p>';
226+
};
227+
228+
Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, $callback);
229+
230+
$this->asEditor();
231+
$this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);
232+
233+
$this->assertCount(2, $args);
234+
$this->assertEquals($page->id, $args[1]->id);
235+
$this->assertEquals('<p id="bkmrk-old-content%21">Old content!</p>', $args[0]);
236+
237+
$newPageHtml = $page->refresh()->html;
238+
$this->assertEquals('<p>New Content!</p>', $newPageHtml);
239+
}
240+
241+
public function test_page_content_pre_store_does_not_change_content_if_nothing_returned()
242+
{
243+
$page = $this->entities->page();
244+
Theme::listen(ThemeEvents::PAGE_CONTENT_PRE_STORE, fn() => null);
245+
246+
$this->asEditor();
247+
$this->entities->updatePage($page, ['name' => 'My cool update page!', 'html' => '<p>Old content!</p>']);
248+
249+
$newPageHtml = $page->refresh()->html;
250+
$this->assertEquals('<p id="bkmrk-old-content%21">Old content!</p>', $newPageHtml);
251+
}
252+
253+
public function test_page_content_post_render_fires_on_page_view()
254+
{
255+
$page = $this->entities->page();
256+
$page->html = '<p>Old content!</p>';
257+
$page->save();
258+
259+
$args = [];
260+
$callback = function (...$eventArgs) use (&$args) {
261+
$args = $eventArgs;
262+
return '<p>New postrendercontentforyou!</p>';
263+
};
264+
265+
Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);
266+
267+
$resp = $this->asEditor()->get($page->getUrl());
268+
$resp->assertSee('<p>New postrendercontentforyou!</p>', false);
269+
270+
$this->assertCount(2, $args);
271+
$this->assertEquals($page->id, $args[1]->id);
272+
$this->assertEquals('<p>Old content!</p>', $args[0]);
273+
}
274+
275+
public function test_page_content_post_render_returns_original_content_if_no_return()
276+
{
277+
$page = $this->entities->page();
278+
$page->html = '<p>Old content!</p>';
279+
$page->save();
280+
281+
$args = [];
282+
$callback = function (...$eventArgs) use (&$args) {
283+
$args = $eventArgs;
284+
};
285+
286+
Theme::listen(ThemeEvents::PAGE_CONTENT_POST_RENDER, $callback);
287+
288+
$resp = $this->asEditor()->get($page->getUrl());
289+
$resp->assertSee('<p>Old content!</p>', false);
290+
291+
$this->assertCount(2, $args);
292+
}
293+
218294
public function test_page_include_parse()
219295
{
220296
/** @var Page $page */

0 commit comments

Comments
 (0)