]> BookStack Code Mirror - bookstack/commitdiff
Includes: Added back support for parse theme event 4688/head
authorDan Brown <redacted>
Mon, 27 Nov 2023 21:38:43 +0000 (21:38 +0000)
committerDan Brown <redacted>
Mon, 27 Nov 2023 21:39:43 +0000 (21:39 +0000)
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.

app/Entities/Tools/PageContent.php
app/Entities/Tools/PageIncludeContent.php
app/Entities/Tools/PageIncludeParser.php
app/Theming/ThemeEvents.php
app/Theming/ThemeService.php
tests/Entity/PageContentTest.php
tests/Unit/PageIncludeParserTest.php

index 22190f03f61913a48326ce0c24f64bbb12339140..99070ae8935eaf033496fcd54bc40c533012bedb 100644 (file)
@@ -5,10 +5,13 @@ namespace BookStack\Entities\Tools;
 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;
@@ -280,18 +283,11 @@ class PageContent
         }
 
         $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();
         }
 
@@ -308,6 +304,39 @@ class PageContent
         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.
      */
index 2e173859d780210a66df2c173fe608e54a611d83..7c4f943c831d4f685efa20a1a4fbcaec9963012a 100644 (file)
@@ -10,47 +10,53 @@ class PageIncludeContent
     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
@@ -65,4 +71,15 @@ class PageIncludeContent
     {
         return $this->contents;
     }
+
+    public function toHtml(): string
+    {
+        $html = '';
+
+        foreach ($this->contents as $content) {
+            $html .= $content->ownerDocument->saveHTML($content);
+        }
+
+        return $html;
+    }
 }
index 02af3fce99aef19a751497620fef76e78522469e..f1fbfba03278c8ad1d2589986284bcf2bb5b0913 100644 (file)
@@ -19,6 +19,9 @@ class PageIncludeParser
      */
     protected array $toCleanup = [];
 
+    /**
+     * @param Closure(PageIncludeTag $tag): PageContent $pageContentForId
+     */
     public function __construct(
         protected HtmlDocument $doc,
         protected Closure $pageContentForId,
@@ -35,8 +38,8 @@ class PageIncludeParser
         $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);
index 9e14707dea055dc73014f2f0fd0a0853b259649f..3d8cd416789b48b039b6e63b85a6e3c1e794a013 100644 (file)
@@ -2,8 +2,6 @@
 
 namespace BookStack\Theming;
 
-use BookStack\Entities\Models\Page;
-
 /**
  * The ThemeEvents used within BookStack.
  *
@@ -93,8 +91,8 @@ class ThemeEvents
      *
      * @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';
 
index 31a7d3c64d32c778020474fbcc43837467304c61..0c252653699dfa48e8d34deab8423f4c67775a8d 100644 (file)
@@ -48,6 +48,14 @@ class ThemeService
         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.
      */
index 5b46c08a304477668607f3c865fae7f31024a0a2..958598fda8f400bc51654f3e50537e1dc0b9c7c4 100644 (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);
     }
 
+    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();
index fc071cf79c8a2ed33ec182da5c1f54a0f456f0c0..83fded4367d606b6c9d72d2f76a81b39b59ad5bb 100644 (file)
@@ -2,7 +2,9 @@
 
 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;
 
@@ -227,8 +229,9 @@ class PageIncludeParserTest extends 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();