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;
use BookStack\Util\HtmlDocument;
+use Closure;
use DOMElement;
use DOMNode;
use DOMNodeList;
}
$doc = new HtmlDocument($html);
-
- $contentProvider = function (int $id) use ($blankIncludes) {
- if ($blankIncludes) {
- return '';
- }
- return Page::visible()->find($id)->html ?? '';
- };
-
+ $contentProvider = $this->getContentProviderClosure($blankIncludes);
$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();
}
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.
*/
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(
- string $html,
- PageIncludeTag $tag,
+ protected array $contents,
+ 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)) {
- return;
+ return new self([], true);
}
$doc = new HtmlDocument($html);
$sectionId = $tag->getSectionId();
if (!$sectionId) {
- $this->contents = [...$doc->getBodyChildren()];
- $this->isTopLevel = true;
- return;
+ $contents = [...$doc->getBodyChildren()];
+ return new self($contents, false);
}
$section = $doc->getElementById($sectionId);
if (!$section) {
- return;
+ return new self([], true);
}
$isTopLevel = in_array(strtolower($section->nodeName), static::$topLevelTags);
- $this->isTopLevel = $isTopLevel;
- $this->contents = $isTopLevel ? [$section] : [...$section->childNodes];
+ $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
{
- return !$this->isTopLevel;
+ return $this->isInline;
}
public function isEmpty(): bool
{
return $this->contents;
}
+
+ public function toHtml(): string
+ {
+ $html = '';
+
+ foreach ($this->contents as $content) {
+ $html .= $content->ownerDocument->saveHTML($content);
+ }
+
+ return $html;
+ }
}
*/
protected array $toCleanup = [];
+ /**
+ * @param Closure(PageIncludeTag $tag): PageContent $pageContentForId
+ */
public function __construct(
protected HtmlDocument $doc,
protected Closure $pageContentForId,
$tags = $this->locateAndIsolateIncludeTags();
foreach ($tags as $tag) {
- $htmlContent = $this->pageContentForId->call($this, $tag->getPageId());
- $content = new PageIncludeContent($htmlContent, $tag);
+ /** @var PageIncludeContent $content */
+ $content = $this->pageContentForId->call($this, $tag);
if (!$content->isInline()) {
$parentP = $this->getParentParagraph($tag->domNode);
namespace BookStack\Theming;
-use BookStack\Entities\Models\Page;
-
/**
* The ThemeEvents used within BookStack.
*
*
* @param string $tagReference
* @param string $replacementHTML
- * @param Page $currentPage
- * @param ?Page $referencedPage
+ * @param \BookStack\Entities\Models\Page $currentPage
+ * @param ?\BookStack\Entities\Models\Page $referencedPage
*/
const PAGE_INCLUDE_PARSE = 'page_include_parse';
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.
*/
$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()
{
$this->asEditor();
namespace Tests\Unit;
+use BookStack\Entities\Tools\PageIncludeContent;
use BookStack\Entities\Tools\PageIncludeParser;
+use BookStack\Entities\Tools\PageIncludeTag;
use BookStack\Util\HtmlDocument;
use Tests\TestCase;
protected function runParserTest(string $html, array $contentById, string $expected): void
{
$doc = new HtmlDocument($html);
- $parser = new PageIncludeParser($doc, function (int $id) use ($contentById) {
- return $contentById[strval($id)] ?? '';
+ $parser = new PageIncludeParser($doc, function (PageIncludeTag $tag) use ($contentById): PageIncludeContent {
+ $html = $contentById[strval($tag->getPageId())] ?? '';
+ return PageIncludeContent::fromHtmlAndTag($html, $tag);
});
$parser->parse();