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