0
0
Fork 0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-05-05 16:50:12 +00:00

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.
This commit is contained in:
Dan Brown 2023-11-27 21:38:43 +00:00
parent b569827114
commit 652d5417bf
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
7 changed files with 106 additions and 35 deletions

View file

@ -5,10 +5,13 @@ namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\MarkdownToHtml; use BookStack\Entities\Tools\Markdown\MarkdownToHtml;
use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService; use BookStack\Uploads\ImageService;
use BookStack\Util\HtmlContentFilter; use BookStack\Util\HtmlContentFilter;
use BookStack\Util\HtmlDocument; use BookStack\Util\HtmlDocument;
use Closure;
use DOMElement; use DOMElement;
use DOMNode; use DOMNode;
use DOMNodeList; use DOMNodeList;
@ -280,18 +283,11 @@ class PageContent
} }
$doc = new HtmlDocument($html); $doc = new HtmlDocument($html);
$contentProvider = $this->getContentProviderClosure($blankIncludes);
$contentProvider = function (int $id) use ($blankIncludes) {
if ($blankIncludes) {
return '';
}
return Page::visible()->find($id)->html ?? '';
};
$parser = new PageIncludeParser($doc, $contentProvider); $parser = new PageIncludeParser($doc, $contentProvider);
$nodesAdded = 1;
for ($includeDepth = 0; $includeDepth < 1 && $nodesAdded !== 0; $includeDepth++) { $nodesAdded = 1;
for ($includeDepth = 0; $includeDepth < 3 && $nodesAdded !== 0; $includeDepth++) {
$nodesAdded = $parser->parse(); $nodesAdded = $parser->parse();
} }
@ -308,6 +304,39 @@ class PageContent
return $doc->getBodyInnerHtml(); return $doc->getBodyInnerHtml();
} }
/**
* Get the closure used to fetch content for page includes.
*/
protected function getContentProviderClosure(bool $blankIncludes): Closure
{
$contextPage = $this->page;
return function (PageIncludeTag $tag) use ($blankIncludes, $contextPage): PageIncludeContent {
if ($blankIncludes) {
return PageIncludeContent::fromHtmlAndTag('', $tag);
}
$matchedPage = Page::visible()->find($tag->getPageId());
$content = PageIncludeContent::fromHtmlAndTag($matchedPage->html ?? '', $tag);
if (Theme::hasListeners(ThemeEvents::PAGE_INCLUDE_PARSE)) {
$themeReplacement = Theme::dispatch(
ThemeEvents::PAGE_INCLUDE_PARSE,
$tag->tagContent,
$content->toHtml(),
clone $contextPage,
$matchedPage ? (clone $matchedPage) : null,
);
if ($themeReplacement !== null) {
$content = PageIncludeContent::fromInlineHtml(strval($themeReplacement));
}
}
return $content;
};
}
/** /**
* Parse the headers on the page to get a navigation menu. * Parse the headers on the page to get a navigation menu.
*/ */

View file

@ -10,47 +10,53 @@ class PageIncludeContent
protected static array $topLevelTags = ['table', 'ul', 'ol', 'pre']; protected static array $topLevelTags = ['table', 'ul', 'ol', 'pre'];
/** /**
* @var DOMNode[] * @param DOMNode[] $contents
* @param bool $isInline
*/ */
protected array $contents = [];
protected bool $isTopLevel = false;
public function __construct( public function __construct(
string $html, protected array $contents,
PageIncludeTag $tag, protected bool $isInline,
) { ) {
$this->parseHtml($html, $tag);
} }
protected function parseHtml(string $html, PageIncludeTag $tag): void public static function fromHtmlAndTag(string $html, PageIncludeTag $tag): self
{ {
if (empty($html)) { if (empty($html)) {
return; return new self([], true);
} }
$doc = new HtmlDocument($html); $doc = new HtmlDocument($html);
$sectionId = $tag->getSectionId(); $sectionId = $tag->getSectionId();
if (!$sectionId) { if (!$sectionId) {
$this->contents = [...$doc->getBodyChildren()]; $contents = [...$doc->getBodyChildren()];
$this->isTopLevel = true; return new self($contents, false);
return;
} }
$section = $doc->getElementById($sectionId); $section = $doc->getElementById($sectionId);
if (!$section) { if (!$section) {
return; return new self([], true);
} }
$isTopLevel = in_array(strtolower($section->nodeName), static::$topLevelTags); $isTopLevel = in_array(strtolower($section->nodeName), static::$topLevelTags);
$this->isTopLevel = $isTopLevel; $contents = $isTopLevel ? [$section] : [...$section->childNodes];
$this->contents = $isTopLevel ? [$section] : [...$section->childNodes]; return new self($contents, !$isTopLevel);
}
public static function fromInlineHtml(string $html): self
{
if (empty($html)) {
return new self([], true);
}
$doc = new HtmlDocument($html);
return new self([...$doc->getBodyChildren()], true);
} }
public function isInline(): bool public function isInline(): bool
{ {
return !$this->isTopLevel; return $this->isInline;
} }
public function isEmpty(): bool public function isEmpty(): bool
@ -65,4 +71,15 @@ class PageIncludeContent
{ {
return $this->contents; return $this->contents;
} }
public function toHtml(): string
{
$html = '';
foreach ($this->contents as $content) {
$html .= $content->ownerDocument->saveHTML($content);
}
return $html;
}
} }

View file

@ -19,6 +19,9 @@ class PageIncludeParser
*/ */
protected array $toCleanup = []; protected array $toCleanup = [];
/**
* @param Closure(PageIncludeTag $tag): PageContent $pageContentForId
*/
public function __construct( public function __construct(
protected HtmlDocument $doc, protected HtmlDocument $doc,
protected Closure $pageContentForId, protected Closure $pageContentForId,
@ -35,8 +38,8 @@ class PageIncludeParser
$tags = $this->locateAndIsolateIncludeTags(); $tags = $this->locateAndIsolateIncludeTags();
foreach ($tags as $tag) { foreach ($tags as $tag) {
$htmlContent = $this->pageContentForId->call($this, $tag->getPageId()); /** @var PageIncludeContent $content */
$content = new PageIncludeContent($htmlContent, $tag); $content = $this->pageContentForId->call($this, $tag);
if (!$content->isInline()) { if (!$content->isInline()) {
$parentP = $this->getParentParagraph($tag->domNode); $parentP = $this->getParentParagraph($tag->domNode);

View file

@ -2,8 +2,6 @@
namespace BookStack\Theming; namespace BookStack\Theming;
use BookStack\Entities\Models\Page;
/** /**
* The ThemeEvents used within BookStack. * The ThemeEvents used within BookStack.
* *
@ -93,8 +91,8 @@ class ThemeEvents
* *
* @param string $tagReference * @param string $tagReference
* @param string $replacementHTML * @param string $replacementHTML
* @param Page $currentPage * @param \BookStack\Entities\Models\Page $currentPage
* @param ?Page $referencedPage * @param ?\BookStack\Entities\Models\Page $referencedPage
*/ */
const PAGE_INCLUDE_PARSE = 'page_include_parse'; const PAGE_INCLUDE_PARSE = 'page_include_parse';

View file

@ -48,6 +48,14 @@ class ThemeService
return null; return null;
} }
/**
* Check if there are listeners registered for the given event name.
*/
public function hasListeners(string $event): bool
{
return count($this->listeners[$event] ?? []) > 0;
}
/** /**
* Register a new custom artisan command to be available. * Register a new custom artisan command to be available.
*/ */

View file

@ -88,6 +88,19 @@ class PageContentTest extends TestCase
$this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag); $this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);
} }
public function test_page_includes_to_nonexisting_pages_does_not_error()
{
$page = $this->entities->page();
$missingId = Page::query()->max('id') + 1;
$tag = "{{@{$missingId}}}";
$page->html = '<p id="bkmrk-test">Hello Barry ' . $tag . '</p>';
$page->save();
$pageResp = $this->asEditor()->get($page->getUrl());
$pageResp->assertOk();
$pageResp->assertSee('Hello Barry');
}
public function test_page_content_scripts_removed_by_default() public function test_page_content_scripts_removed_by_default()
{ {
$this->asEditor(); $this->asEditor();

View file

@ -2,7 +2,9 @@
namespace Tests\Unit; namespace Tests\Unit;
use BookStack\Entities\Tools\PageIncludeContent;
use BookStack\Entities\Tools\PageIncludeParser; use BookStack\Entities\Tools\PageIncludeParser;
use BookStack\Entities\Tools\PageIncludeTag;
use BookStack\Util\HtmlDocument; use BookStack\Util\HtmlDocument;
use Tests\TestCase; use Tests\TestCase;
@ -227,8 +229,9 @@ class PageIncludeParserTest extends TestCase
protected function runParserTest(string $html, array $contentById, string $expected): void protected function runParserTest(string $html, array $contentById, string $expected): void
{ {
$doc = new HtmlDocument($html); $doc = new HtmlDocument($html);
$parser = new PageIncludeParser($doc, function (int $id) use ($contentById) { $parser = new PageIncludeParser($doc, function (PageIncludeTag $tag) use ($contentById): PageIncludeContent {
return $contentById[strval($id)] ?? ''; $html = $contentById[strval($tag->getPageId())] ?? '';
return PageIncludeContent::fromHtmlAndTag($html, $tag);
}); });
$parser->parse(); $parser->parse();