Skip to content

Commit 652d541

Browse files
committed
Includes: Added back support for parse theme event
Managed to do this in an API-compatible way although resuling output may differ due to new dom handling in general, although user content is used inline to remain as comptable as possible.
1 parent b569827 commit 652d541

7 files changed

Lines changed: 106 additions & 35 deletions

File tree

app/Entities/Tools/PageContent.php

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
use BookStack\Entities\Models\Page;
66
use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
77
use BookStack\Exceptions\ImageUploadException;
8+
use BookStack\Facades\Theme;
9+
use BookStack\Theming\ThemeEvents;
810
use BookStack\Uploads\ImageRepo;
911
use BookStack\Uploads\ImageService;
1012
use BookStack\Util\HtmlContentFilter;
1113
use BookStack\Util\HtmlDocument;
14+
use Closure;
1215
use DOMElement;
1316
use DOMNode;
1417
use DOMNodeList;
@@ -280,18 +283,11 @@ public function render(bool $blankIncludes = false): string
280283
}
281284

282285
$doc = new HtmlDocument($html);
283-
284-
$contentProvider = function (int $id) use ($blankIncludes) {
285-
if ($blankIncludes) {
286-
return '';
287-
}
288-
return Page::visible()->find($id)->html ?? '';
289-
};
290-
286+
$contentProvider = $this->getContentProviderClosure($blankIncludes);
291287
$parser = new PageIncludeParser($doc, $contentProvider);
292-
$nodesAdded = 1;
293288

294-
for ($includeDepth = 0; $includeDepth < 1 && $nodesAdded !== 0; $includeDepth++) {
289+
$nodesAdded = 1;
290+
for ($includeDepth = 0; $includeDepth < 3 && $nodesAdded !== 0; $includeDepth++) {
295291
$nodesAdded = $parser->parse();
296292
}
297293

@@ -308,6 +304,39 @@ public function render(bool $blankIncludes = false): string
308304
return $doc->getBodyInnerHtml();
309305
}
310306

307+
/**
308+
* Get the closure used to fetch content for page includes.
309+
*/
310+
protected function getContentProviderClosure(bool $blankIncludes): Closure
311+
{
312+
$contextPage = $this->page;
313+
314+
return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage): PageIncludeContent {
315+
if ($blankIncludes) {
316+
return PageIncludeContent::fromHtmlAndTag('', $tag);
317+
}
318+
319+
$matchedPage = Page::visible()->find($tag->getPageId());
320+
$content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);
321+
322+
if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {
323+
$themeReplacement = Theme::dispatch(
324+
ThemeEvents::PAGE_INCLUDE_PARSE,
325+
$tag->tagContent,
326+
$content->toHtml(),
327+
clone $contextPage,
328+
$matchedPage ? (clone $matchedPage) : null,
329+
);
330+
331+
if ($themeReplacement !== null) {
332+
$content = PageIncludeContent::fromInlineHtml(strval($themeReplacement));
333+
}
334+
}
335+
336+
return $content;
337+
};
338+
}
339+
311340
/**
312341
* Parse the headers on the page to get a navigation menu.
313342
*/

app/Entities/Tools/PageIncludeContent.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,53 @@ class PageIncludeContent
1010
protected static array $topLevelTags = ['table', 'ul', 'ol', 'pre'];
1111

1212
/**
13-
* @var DOMNode[]
13+
* @param DOMNode[] $contents
14+
* @param bool $isInline
1415
*/
15-
protected array $contents = [];
16-
17-
protected bool $isTopLevel = false;
18-
1916
public function __construct(
20-
string $html,
21-
PageIncludeTag $tag,
17+
protected array $contents,
18+
protected bool $isInline,
2219
) {
23-
$this->parseHtml($html, $tag);
2420
}
2521

26-
protected function parseHtml(string $html, PageIncludeTag $tag): void
22+
public static function fromHtmlAndTag(string $html, PageIncludeTag $tag): self
2723
{
2824
if (empty($html)) {
29-
return;
25+
return new self([], true);
3026
}
3127

3228
$doc = new HtmlDocument($html);
3329

3430
$sectionId = $tag->getSectionId();
3531
if (!$sectionId) {
36-
$this->contents = [...$doc->getBodyChildren()];
37-
$this->isTopLevel = true;
38-
return;
32+
$contents = [...$doc->getBodyChildren()];
33+
return new self($contents, false);
3934
}
4035

4136
$section = $doc->getElementById($sectionId);
4237
if (!$section) {
43-
return;
38+
return new self([], true);
4439
}
4540

4641
$isTopLevel = in_array(strtolower($section->nodeName), static::$topLevelTags);
47-
$this->isTopLevel = $isTopLevel;
48-
$this->contents = $isTopLevel ? [$section] : [...$section->childNodes];
42+
$contents = $isTopLevel ? [$section] : [...$section->childNodes];
43+
return new self($contents, !$isTopLevel);
44+
}
45+
46+
public static function fromInlineHtml(string $html): self
47+
{
48+
if (empty($html)) {
49+
return new self([], true);
50+
}
51+
52+
$doc = new HtmlDocument($html);
53+
54+
return new self([...$doc->getBodyChildren()], true);
4955
}
5056

5157
public function isInline(): bool
5258
{
53-
return !$this->isTopLevel;
59+
return $this->isInline;
5460
}
5561

5662
public function isEmpty(): bool
@@ -65,4 +71,15 @@ public function toDomNodes(): array
6571
{
6672
return $this->contents;
6773
}
74+
75+
public function toHtml(): string
76+
{
77+
$html = '';
78+
79+
foreach ($this->contents as $content) {
80+
$html .= $content->ownerDocument->saveHTML($content);
81+
}
82+
83+
return $html;
84+
}
6885
}

app/Entities/Tools/PageIncludeParser.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class PageIncludeParser
1919
*/
2020
protected array $toCleanup = [];
2121

22+
/**
23+
* @param Closure(PageIncludeTag $tag): PageContent $pageContentForId
24+
*/
2225
public function __construct(
2326
protected HtmlDocument $doc,
2427
protected Closure $pageContentForId,
@@ -35,8 +38,8 @@ public function parse(): int
3538
$tags = $this->locateAndIsolateIncludeTags();
3639

3740
foreach ($tags as $tag) {
38-
$htmlContent = $this->pageContentForId->call($this, $tag->getPageId());
39-
$content = new PageIncludeContent($htmlContent, $tag);
41+
/** @var PageIncludeContent $content */
42+
$content = $this->pageContentForId->call($this, $tag);
4043

4144
if (!$content->isInline()) {
4245
$parentP = $this->getParentParagraph($tag->domNode);

app/Theming/ThemeEvents.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
namespace BookStack\Theming;
44

5-
use BookStack\Entities\Models\Page;
6-
75
/**
86
* The ThemeEvents used within BookStack.
97
*
@@ -93,8 +91,8 @@ class ThemeEvents
9391
*
9492
* @param string $tagReference
9593
* @param string $replacementHTML
96-
* @param Page $currentPage
97-
* @param ?Page $referencedPage
94+
* @param \BookStack\Entities\Models\Page $currentPage
95+
* @param ?\BookStack\Entities\Models\Page $referencedPage
9896
*/
9997
const PAGE_INCLUDE_PARSE = 'page_include_parse';
10098

app/Theming/ThemeService.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ public function dispatch(string $event, ...$args): mixed
4848
return null;
4949
}
5050

51+
/**
52+
* Check if there are listeners registered for the given event name.
53+
*/
54+
public function hasListeners(string $event): bool
55+
{
56+
return count($this->listeners[$event] ?? []) > 0;
57+
}
58+
5159
/**
5260
* Register a new custom artisan command to be available.
5361
*/

tests/Entity/PageContentTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ public function test_page_includes_can_be_nested_up_to_three_times()
8888
$this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);
8989
}
9090

91+
public function test_page_includes_to_nonexisting_pages_does_not_error()
92+
{
93+
$page = $this->entities->page();
94+
$missingId = Page::query()->max('id') + 1;
95+
$tag = "{{@{$missingId}}}";
96+
$page->html = '<p id="bkmrk-test">Hello Barry ' . $tag . '</p>';
97+
$page->save();
98+
99+
$pageResp = $this->asEditor()->get($page->getUrl());
100+
$pageResp->assertOk();
101+
$pageResp->assertSee('Hello Barry');
102+
}
103+
91104
public function test_page_content_scripts_removed_by_default()
92105
{
93106
$this->asEditor();

tests/Unit/PageIncludeParserTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Tests\Unit;
44

5+
use BookStack\Entities\Tools\PageIncludeContent;
56
use BookStack\Entities\Tools\PageIncludeParser;
7+
use BookStack\Entities\Tools\PageIncludeTag;
68
use BookStack\Util\HtmlDocument;
79
use Tests\TestCase;
810

@@ -227,8 +229,9 @@ public function test_multiple_tags_in_shallow_origin_with_multi_block_content()
227229
protected function runParserTest(string $html, array $contentById, string $expected): void
228230
{
229231
$doc = new HtmlDocument($html);
230-
$parser = new PageIncludeParser($doc, function (int $id) use ($contentById) {
231-
return $contentById[strval($id)] ?? '';
232+
$parser = new PageIncludeParser($doc, function (PageIncludeTag $tag) use ($contentById): PageIncludeContent {
233+
$html = $contentById[strval($tag->getPageId())] ?? '';
234+
return PageIncludeContent::fromHtmlAndTag($html, $tag);
232235
});
233236

234237
$parser->parse();

0 commit comments

Comments
 (0)