]> BookStack Code Mirror - bookstack/blob - app/Entities/Tools/ExportFormatter.php
9317b04316b5b4bb4c3d4feac62977e42785b0c3
[bookstack] / app / Entities / Tools / ExportFormatter.php
1 <?php namespace BookStack\Entities\Tools;
2
3 use BookStack\Entities\Models\Book;
4 use BookStack\Entities\Models\Chapter;
5 use BookStack\Entities\Models\Page;
6 use BookStack\Entities\Tools\Markdown\HtmlToMarkdown;
7 use BookStack\Uploads\ImageService;
8 use DomPDF;
9 use Exception;
10 use SnappyPDF;
11 use Throwable;
12
13 class ExportFormatter
14 {
15
16     protected $imageService;
17
18     /**
19      * ExportService constructor.
20      */
21     public function __construct(ImageService $imageService)
22     {
23         $this->imageService = $imageService;
24     }
25
26     /**
27      * Convert a page to a self-contained HTML file.
28      * Includes required CSS & image content. Images are base64 encoded into the HTML.
29      * @throws Throwable
30      */
31     public function pageToContainedHtml(Page $page)
32     {
33         $page->html = (new PageContent($page))->render();
34         $pageHtml = view('pages.export', [
35             'page' => $page,
36             'format' => 'html',
37         ])->render();
38         return $this->containHtml($pageHtml);
39     }
40
41     /**
42      * Convert a chapter to a self-contained HTML file.
43      * @throws Throwable
44      */
45     public function chapterToContainedHtml(Chapter $chapter)
46     {
47         $pages = $chapter->getVisiblePages();
48         $pages->each(function ($page) {
49             $page->html = (new PageContent($page))->render();
50         });
51         $html = view('chapters.export', [
52             'chapter' => $chapter,
53             'pages' => $pages,
54             'format' => 'html',
55         ])->render();
56         return $this->containHtml($html);
57     }
58
59     /**
60      * Convert a book to a self-contained HTML file.
61      * @throws Throwable
62      */
63     public function bookToContainedHtml(Book $book)
64     {
65         $bookTree = (new BookContents($book))->getTree(false, true);
66         $html = view('books.export', [
67             'book' => $book,
68             'bookChildren' => $bookTree,
69             'format' => 'html',
70         ])->render();
71         return $this->containHtml($html);
72     }
73
74     /**
75      * Convert a page to a PDF file.
76      * @throws Throwable
77      */
78     public function pageToPdf(Page $page)
79     {
80         $page->html = (new PageContent($page))->render();
81         $html = view('pages.export', [
82             'page' => $page,
83             'format' => 'pdf',
84         ])->render();
85         return $this->htmlToPdf($html);
86     }
87
88     /**
89      * Convert a chapter to a PDF file.
90      * @throws Throwable
91      */
92     public function chapterToPdf(Chapter $chapter)
93     {
94         $pages = $chapter->getVisiblePages();
95         $pages->each(function ($page) {
96             $page->html = (new PageContent($page))->render();
97         });
98
99         $html = view('chapters.export', [
100             'chapter' => $chapter,
101             'pages' => $pages,
102             'format' => 'pdf',
103         ])->render();
104
105         return $this->htmlToPdf($html);
106     }
107
108     /**
109      * Convert a book to a PDF file.
110      * @throws Throwable
111      */
112     public function bookToPdf(Book $book)
113     {
114         $bookTree = (new BookContents($book))->getTree(false, true);
115         $html = view('books.export', [
116             'book' => $book,
117             'bookChildren' => $bookTree,
118             'format' => 'pdf',
119         ])->render();
120         return $this->htmlToPdf($html);
121     }
122
123     /**
124      * Convert normal web-page HTML to a PDF.
125      * @throws Exception
126      */
127     protected function htmlToPdf(string $html): string
128     {
129         $containedHtml = $this->containHtml($html);
130         $useWKHTML = config('snappy.pdf.binary') !== false;
131         if ($useWKHTML) {
132             $pdf = SnappyPDF::loadHTML($containedHtml);
133             $pdf->setOption('print-media-type', true);
134         } else {
135             $pdf = DomPDF::loadHTML($containedHtml);
136         }
137         return $pdf->output();
138     }
139
140     /**
141      * Bundle of the contents of a html file to be self-contained.
142      * @throws Exception
143      */
144     protected function containHtml(string $htmlContent): string
145     {
146         $imageTagsOutput = [];
147         preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
148
149         // Replace image src with base64 encoded image strings
150         if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
151             foreach ($imageTagsOutput[0] as $index => $imgMatch) {
152                 $oldImgTagString = $imgMatch;
153                 $srcString = $imageTagsOutput[2][$index];
154                 $imageEncoded = $this->imageService->imageUriToBase64($srcString);
155                 if ($imageEncoded === null) {
156                     $imageEncoded = $srcString;
157                 }
158                 $newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
159                 $htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
160             }
161         }
162
163         $linksOutput = [];
164         preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
165
166         // Replace image src with base64 encoded image strings
167         if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
168             foreach ($linksOutput[0] as $index => $linkMatch) {
169                 $oldLinkString = $linkMatch;
170                 $srcString = $linksOutput[2][$index];
171                 if (strpos(trim($srcString), 'http') !== 0) {
172                     $newSrcString = url($srcString);
173                     $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
174                     $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
175                 }
176             }
177         }
178
179         // Replace any relative links with system domain
180         return $htmlContent;
181     }
182
183     /**
184      * Converts the page contents into simple plain text.
185      * This method filters any bad looking content to provide a nice final output.
186      */
187     public function pageToPlainText(Page $page): string
188     {
189         $html = (new PageContent($page))->render();
190         $text = strip_tags($html);
191         // Replace multiple spaces with single spaces
192         $text = preg_replace('/\ {2,}/', ' ', $text);
193         // Reduce multiple horrid whitespace characters.
194         $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
195         $text = html_entity_decode($text);
196         // Add title
197         $text = $page->name . "\n\n" . $text;
198         return $text;
199     }
200
201     /**
202      * Convert a chapter into a plain text string.
203      */
204     public function chapterToPlainText(Chapter $chapter): string
205     {
206         $text = $chapter->name . "\n\n";
207         $text .= $chapter->description . "\n\n";
208         foreach ($chapter->getVisiblePages() as $page) {
209             $text .= $this->pageToPlainText($page);
210         }
211         return $text;
212     }
213
214     /**
215      * Convert a book into a plain text string.
216      */
217     public function bookToPlainText(Book $book): string
218     {
219         $bookTree = (new BookContents($book))->getTree(false, false);
220         $text = $book->name . "\n\n";
221         foreach ($bookTree as $bookChild) {
222             if ($bookChild->isA('chapter')) {
223                 $text .= $this->chapterToPlainText($bookChild);
224             } else {
225                 $text .= $this->pageToPlainText($bookChild);
226             }
227         }
228         return $text;
229     }
230
231     /**
232      * Convert a page to a Markdown file.
233      */
234     public function pageToMarkdown(Page $page): string
235     {
236         if ($page->markdown) {
237             return "# " . $page->name . "\n\n" . $page->markdown;
238         }
239
240         return "# " . $page->name . "\n\n" . (new HtmlToMarkdown($page->html))->convert();
241     }
242
243     /**
244      * Convert a chapter to a Markdown file.
245      */
246     public function chapterToMarkdown(Chapter $chapter): string
247     {
248         $text = "# " . $chapter->name . "\n\n";
249         $text .= $chapter->description . "\n\n";
250         foreach ($chapter->pages as $page) {
251             $text .= $this->pageToMarkdown($page) . "\n\n";
252         }
253         return $text;
254     }
255
256     /**
257      * Convert a book into a plain text string.
258      */
259     public function bookToMarkdown(Book $book): string
260     {
261         $bookTree = (new BookContents($book))->getTree(false, true);
262         $text = "# " . $book->name . "\n\n";
263         foreach ($bookTree as $bookChild) {
264             if ($bookChild instanceof Chapter) {
265                 $text .= $this->chapterToMarkdown($bookChild);
266             } else {
267                 $text .= $this->pageToMarkdown($bookChild);
268             }
269         }
270         return $text;
271     }
272 }