]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/ExportFormatter.php
Added translation string for tasklist WYSIWYG action
[bookstack] / app / Entities / Tools / ExportFormatter.php
1 <?php
2
3 namespace BookStack\Entities\Tools;
4
5 use BookStack\Entities\Models\Book;
6 use BookStack\Entities\Models\Chapter;
7 use BookStack\Entities\Models\Page;
8 use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
9 use BookStack\Uploads\ImageService;
10 use BookStack\Util\CspService;
11 use DOMDocument;
12 use DOMElement;
13 use DOMXPath;
14 use Exception;
15 use Throwable;
16
17 class ExportFormatter
18 {
19     protected ImageService $imageService;
20     protected PdfGenerator $pdfGenerator;
21     protected CspService $cspService;
22
23     /**
24      * ExportService constructor.
25      */
26     public function __construct(ImageService $imageService, PdfGenerator $pdfGenerator, CspService $cspService)
27     {
28         $this->imageService = $imageService;
29         $this->pdfGenerator = $pdfGenerator;
30         $this->cspService = $cspService;
31     }
32
33     /**
34      * Convert a page to a self-contained HTML file.
35      * Includes required CSS & image content. Images are base64 encoded into the HTML.
36      *
37      * @throws Throwable
38      */
39     public function pageToContainedHtml(Page $page)
40     {
41         $page->html = (new PageContent($page))->render();
42         $pageHtml = view('pages.export', [
43             'page'       => $page,
44             'format'     => 'html',
45             'cspContent' => $this->cspService->getCspMetaTagValue(),
46         ])->render();
47
48         return $this->containHtml($pageHtml);
49     }
50
51     /**
52      * Convert a chapter to a self-contained HTML file.
53      *
54      * @throws Throwable
55      */
56     public function chapterToContainedHtml(Chapter $chapter)
57     {
58         $pages = $chapter->getVisiblePages();
59         $pages->each(function ($page) {
60             $page->html = (new PageContent($page))->render();
61         });
62         $html = view('chapters.export', [
63             'chapter'    => $chapter,
64             'pages'      => $pages,
65             'format'     => 'html',
66             'cspContent' => $this->cspService->getCspMetaTagValue(),
67         ])->render();
68
69         return $this->containHtml($html);
70     }
71
72     /**
73      * Convert a book to a self-contained HTML file.
74      *
75      * @throws Throwable
76      */
77     public function bookToContainedHtml(Book $book)
78     {
79         $bookTree = (new BookContents($book))->getTree(false, true);
80         $html = view('books.export', [
81             'book'         => $book,
82             'bookChildren' => $bookTree,
83             'format'       => 'html',
84             'cspContent'   => $this->cspService->getCspMetaTagValue(),
85         ])->render();
86
87         return $this->containHtml($html);
88     }
89
90     /**
91      * Convert a page to a PDF file.
92      *
93      * @throws Throwable
94      */
95     public function pageToPdf(Page $page)
96     {
97         $page->html = (new PageContent($page))->render();
98         $html = view('pages.export', [
99             'page'   => $page,
100             'format' => 'pdf',
101             'engine' => $this->pdfGenerator->getActiveEngine(),
102         ])->render();
103
104         return $this->htmlToPdf($html);
105     }
106
107     /**
108      * Convert a chapter to a PDF file.
109      *
110      * @throws Throwable
111      */
112     public function chapterToPdf(Chapter $chapter)
113     {
114         $pages = $chapter->getVisiblePages();
115         $pages->each(function ($page) {
116             $page->html = (new PageContent($page))->render();
117         });
118
119         $html = view('chapters.export', [
120             'chapter' => $chapter,
121             'pages'   => $pages,
122             'format'  => 'pdf',
123             'engine'  => $this->pdfGenerator->getActiveEngine(),
124         ])->render();
125
126         return $this->htmlToPdf($html);
127     }
128
129     /**
130      * Convert a book to a PDF file.
131      *
132      * @throws Throwable
133      */
134     public function bookToPdf(Book $book)
135     {
136         $bookTree = (new BookContents($book))->getTree(false, true);
137         $html = view('books.export', [
138             'book'         => $book,
139             'bookChildren' => $bookTree,
140             'format'       => 'pdf',
141             'engine'       => $this->pdfGenerator->getActiveEngine(),
142         ])->render();
143
144         return $this->htmlToPdf($html);
145     }
146
147     /**
148      * Convert normal web-page HTML to a PDF.
149      *
150      * @throws Exception
151      */
152     protected function htmlToPdf(string $html): string
153     {
154         $html = $this->containHtml($html);
155         $html = $this->replaceIframesWithLinks($html);
156         $html = $this->openDetailElements($html);
157
158         return $this->pdfGenerator->fromHtml($html);
159     }
160
161     /**
162      * Within the given HTML content, Open any detail blocks.
163      */
164     protected function openDetailElements(string $html): string
165     {
166         libxml_use_internal_errors(true);
167
168         $doc = new DOMDocument();
169         $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
170         $xPath = new DOMXPath($doc);
171
172         $details = $xPath->query('//details');
173         /** @var DOMElement $detail */
174         foreach ($details as $detail) {
175             $detail->setAttribute('open', 'open');
176         }
177
178         return $doc->saveHTML();
179     }
180
181     /**
182      * Within the given HTML content, replace any iframe elements
183      * with anchor links within paragraph blocks.
184      */
185     protected function replaceIframesWithLinks(string $html): string
186     {
187         libxml_use_internal_errors(true);
188
189         $doc = new DOMDocument();
190         $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
191         $xPath = new DOMXPath($doc);
192
193         $iframes = $xPath->query('//iframe');
194         /** @var DOMElement $iframe */
195         foreach ($iframes as $iframe) {
196             $link = $iframe->getAttribute('src');
197             if (strpos($link, '//') === 0) {
198                 $link = 'https:' . $link;
199             }
200
201             $anchor = $doc->createElement('a', $link);
202             $anchor->setAttribute('href', $link);
203             $paragraph = $doc->createElement('p');
204             $paragraph->appendChild($anchor);
205             $iframe->parentNode->replaceChild($paragraph, $iframe);
206         }
207
208         return $doc->saveHTML();
209     }
210
211     /**
212      * Bundle of the contents of a html file to be self-contained.
213      *
214      * @throws Exception
215      */
216     protected function containHtml(string $htmlContent): string
217     {
218         $imageTagsOutput = [];
219         preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
220
221         // Replace image src with base64 encoded image strings
222         if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
223             foreach ($imageTagsOutput[0] as $index => $imgMatch) {
224                 $oldImgTagString = $imgMatch;
225                 $srcString = $imageTagsOutput[2][$index];
226                 $imageEncoded = $this->imageService->imageUriToBase64($srcString);
227                 if ($imageEncoded === null) {
228                     $imageEncoded = $srcString;
229                 }
230                 $newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
231                 $htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
232             }
233         }
234
235         $linksOutput = [];
236         preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
237
238         // Replace image src with base64 encoded image strings
239         if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
240             foreach ($linksOutput[0] as $index => $linkMatch) {
241                 $oldLinkString = $linkMatch;
242                 $srcString = $linksOutput[2][$index];
243                 if (strpos(trim($srcString), 'http') !== 0) {
244                     $newSrcString = url($srcString);
245                     $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
246                     $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
247                 }
248             }
249         }
250
251         // Replace any relative links with system domain
252         return $htmlContent;
253     }
254
255     /**
256      * Converts the page contents into simple plain text.
257      * This method filters any bad looking content to provide a nice final output.
258      */
259     public function pageToPlainText(Page $page): string
260     {
261         $html = (new PageContent($page))->render();
262         $text = strip_tags($html);
263         // Replace multiple spaces with single spaces
264         $text = preg_replace('/\ {2,}/', ' ', $text);
265         // Reduce multiple horrid whitespace characters.
266         $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
267         $text = html_entity_decode($text);
268         // Add title
269         $text = $page->name . "\n\n" . $text;
270
271         return $text;
272     }
273
274     /**
275      * Convert a chapter into a plain text string.
276      */
277     public function chapterToPlainText(Chapter $chapter): string
278     {
279         $text = $chapter->name . "\n\n";
280         $text .= $chapter->description . "\n\n";
281         foreach ($chapter->getVisiblePages() as $page) {
282             $text .= $this->pageToPlainText($page);
283         }
284
285         return $text;
286     }
287
288     /**
289      * Convert a book into a plain text string.
290      */
291     public function bookToPlainText(Book $book): string
292     {
293         $bookTree = (new BookContents($book))->getTree(false, false);
294         $text = $book->name . "\n\n";
295         foreach ($bookTree as $bookChild) {
296             if ($bookChild->isA('chapter')) {
297                 $text .= $this->chapterToPlainText($bookChild);
298             } else {
299                 $text .= $this->pageToPlainText($bookChild);
300             }
301         }
302
303         return $text;
304     }
305
306     /**
307      * Convert a page to a Markdown file.
308      */
309     public function pageToMarkdown(Page $page): string
310     {
311         if ($page->markdown) {
312             return '# ' . $page->name . "\n\n" . $page->markdown;
313         }
314
315         return '# ' . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert();
316     }
317
318     /**
319      * Convert a chapter to a Markdown file.
320      */
321     public function chapterToMarkdown(Chapter $chapter): string
322     {
323         $text = '# ' . $chapter->name . "\n\n";
324         $text .= $chapter->description . "\n\n";
325         foreach ($chapter->pages as $page) {
326             $text .= $this->pageToMarkdown($page) . "\n\n";
327         }
328
329         return $text;
330     }
331
332     /**
333      * Convert a book into a plain text string.
334      */
335     public function bookToMarkdown(Book $book): string
336     {
337         $bookTree = (new BookContents($book))->getTree(false, true);
338         $text = '# ' . $book->name . "\n\n";
339         foreach ($bookTree as $bookChild) {
340             if ($bookChild instanceof Chapter) {
341                 $text .= $this->chapterToMarkdown($bookChild);
342             } else {
343                 $text .= $this->pageToMarkdown($bookChild);
344             }
345         }
346
347         return $text;
348     }
349 }