Dan Brown

Added plaintext & basic PDF page Export

...@@ -226,13 +226,22 @@ class PageController extends Controller ...@@ -226,13 +226,22 @@ class PageController extends Controller
226 return redirect($page->getUrl()); 226 return redirect($page->getUrl());
227 } 227 }
228 228
229 + /**
230 + * Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
231 + * https://github.com/barryvdh/laravel-dompdf
232 + * @param $bookSlug
233 + * @param $pageSlug
234 + * @return \Illuminate\Http\Response
235 + */
229 public function exportPdf($bookSlug, $pageSlug) 236 public function exportPdf($bookSlug, $pageSlug)
230 { 237 {
231 $book = $this->bookRepo->getBySlug($bookSlug); 238 $book = $this->bookRepo->getBySlug($bookSlug);
232 $page = $this->pageRepo->getBySlug($pageSlug, $book->id); 239 $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
233 - $cssContent = file_get_contents(public_path('/css/styles.css')); 240 + $pdfContent = $this->exportService->pageToPdf($page);
234 - 241 + return response()->make($pdfContent, 200, [
235 - return $pdf->download($pageSlug . '.pdf'); 242 + 'Content-Type' => 'application/octet-stream',
243 + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.pdf'
244 + ]);
236 } 245 }
237 246
238 /** 247 /**
...@@ -251,4 +260,22 @@ class PageController extends Controller ...@@ -251,4 +260,22 @@ class PageController extends Controller
251 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html' 260 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
252 ]); 261 ]);
253 } 262 }
263 +
264 + /**
265 + * Export a page to a simple plaintext .txt file.
266 + * @param $bookSlug
267 + * @param $pageSlug
268 + * @return \Illuminate\Http\Response
269 + */
270 + public function exportPlainText($bookSlug, $pageSlug)
271 + {
272 + $book = $this->bookRepo->getBySlug($bookSlug);
273 + $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
274 + $containedHtml = $this->exportService->pageToPlainText($page);
275 + return response()->make($containedHtml, 200, [
276 + 'Content-Type' => 'application/octet-stream',
277 + 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.txt'
278 + ]);
279 + }
280 +
254 } 281 }
......
...@@ -24,6 +24,7 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -24,6 +24,7 @@ Route::group(['middleware' => 'auth'], function () {
24 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 24 Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
25 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf'); 25 Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
26 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml'); 26 Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
27 + Route::get('/{bookSlug}/page/{pageSlug}/export/plaintext', 'PageController@exportPlainText');
27 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit'); 28 Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
28 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); 29 Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
29 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); 30 Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
...@@ -46,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -46,7 +47,6 @@ Route::group(['middleware' => 'auth'], function () {
46 47
47 }); 48 });
48 49
49 -
50 // Users 50 // Users
51 Route::get('/users', 'UserController@index'); 51 Route::get('/users', 'UserController@index');
52 Route::get('/users/create', 'UserController@create'); 52 Route::get('/users/create', 'UserController@create');
......
...@@ -6,7 +6,6 @@ use BookStack\Page; ...@@ -6,7 +6,6 @@ use BookStack\Page;
6 class ExportService 6 class ExportService
7 { 7 {
8 8
9 -
10 /** 9 /**
11 * Convert a page to a self-contained HTML file. 10 * Convert a page to a self-contained HTML file.
12 * Includes required CSS & image content. Images are base64 encoded into the HTML. 11 * Includes required CSS & image content. Images are base64 encoded into the HTML.
...@@ -16,10 +15,33 @@ class ExportService ...@@ -16,10 +15,33 @@ class ExportService
16 public function pageToContainedHtml(Page $page) 15 public function pageToContainedHtml(Page $page)
17 { 16 {
18 $cssContent = file_get_contents(public_path('/css/export-styles.css')); 17 $cssContent = file_get_contents(public_path('/css/export-styles.css'));
18 + $pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
19 + return $this->containHtml($pageHtml);
20 + }
21 +
22 + /**
23 + * Convert a page to a pdf file.
24 + * @param Page $page
25 + * @return mixed|string
26 + */
27 + public function pageToPdf(Page $page)
28 + {
29 + $cssContent = file_get_contents(public_path('/css/export-styles.css'));
19 $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render(); 30 $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
31 + $containedHtml = $this->containHtml($pageHtml);
32 + $pdf = \PDF::loadHTML($containedHtml);
33 + return $pdf->output();
34 + }
20 35
36 + /**
37 + * Bundle of the contents of a html file to be self-contained.
38 + * @param $htmlContent
39 + * @return mixed|string
40 + */
41 + protected function containHtml($htmlContent)
42 + {
21 $imageTagsOutput = []; 43 $imageTagsOutput = [];
22 - preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $pageHtml, $imageTagsOutput); 44 + preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
23 45
24 // Replace image src with base64 encoded image strings 46 // Replace image src with base64 encoded image strings
25 if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) { 47 if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
...@@ -34,12 +56,12 @@ class ExportService ...@@ -34,12 +56,12 @@ class ExportService
34 $imageContent = file_get_contents($pathString); 56 $imageContent = file_get_contents($pathString);
35 $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent); 57 $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
36 $newImageString = str_replace($srcString, $imageEncoded, $oldImgString); 58 $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
37 - $pageHtml = str_replace($oldImgString, $newImageString, $pageHtml); 59 + $htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
38 } 60 }
39 } 61 }
40 62
41 $linksOutput = []; 63 $linksOutput = [];
42 - preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $pageHtml, $linksOutput); 64 + preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
43 65
44 // Replace image src with base64 encoded image strings 66 // Replace image src with base64 encoded image strings
45 if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) { 67 if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
...@@ -49,13 +71,45 @@ class ExportService ...@@ -49,13 +71,45 @@ class ExportService
49 if (strpos(trim($srcString), 'http') !== 0) { 71 if (strpos(trim($srcString), 'http') !== 0) {
50 $newSrcString = url($srcString); 72 $newSrcString = url($srcString);
51 $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString); 73 $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
52 - $pageHtml = str_replace($oldLinkString, $newLinkString, $pageHtml); 74 + $htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
53 } 75 }
54 } 76 }
55 } 77 }
56 78
57 // Replace any relative links with system domain 79 // Replace any relative links with system domain
58 - return $pageHtml; 80 + return $htmlContent;
81 + }
82 +
83 + /**
84 + * Converts the page contents into simple plain text.
85 + * This method filters any bad looking content to
86 + * provide a nice final output.
87 + * @param Page $page
88 + * @return mixed
89 + */
90 + public function pageToPlainText(Page $page)
91 + {
92 + $text = $page->text;
93 + // Replace multiple spaces with single spaces
94 + $text = preg_replace('/\ {2,}/', ' ', $text);
95 + // Reduce multiple horrid whitespace characters.
96 + $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text);
97 + $text = html_entity_decode($text);
98 + // Add title
99 + $text = $page->name . "\n\n" . $text;
100 + return $text;
59 } 101 }
60 102
61 -}
...\ No newline at end of file ...\ No newline at end of file
103 +}
104 +
105 +
106 +
107 +
108 +
109 +
110 +
111 +
112 +
113 +
114 +
115 +
......
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
11 "laravel/socialite": "^2.0", 11 "laravel/socialite": "^2.0",
12 "barryvdh/laravel-ide-helper": "^2.1", 12 "barryvdh/laravel-ide-helper": "^2.1",
13 "barryvdh/laravel-debugbar": "^2.0", 13 "barryvdh/laravel-debugbar": "^2.0",
14 - "league/flysystem-aws-s3-v3": "^1.0" 14 + "league/flysystem-aws-s3-v3": "^1.0",
15 + "barryvdh/laravel-dompdf": "0.6.*"
15 }, 16 },
16 "require-dev": { 17 "require-dev": {
17 "fzaninotto/faker": "~1.4", 18 "fzaninotto/faker": "~1.4",
......
...@@ -143,6 +143,7 @@ return [ ...@@ -143,6 +143,7 @@ return [
143 * Third Party 143 * Third Party
144 */ 144 */
145 Intervention\Image\ImageServiceProvider::class, 145 Intervention\Image\ImageServiceProvider::class,
146 + Barryvdh\DomPDF\ServiceProvider::class,
146 Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, 147 Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
147 Barryvdh\Debugbar\ServiceProvider::class, 148 Barryvdh\Debugbar\ServiceProvider::class,
148 149
...@@ -210,6 +211,7 @@ return [ ...@@ -210,6 +211,7 @@ return [
210 */ 211 */
211 212
212 'ImageTool' => Intervention\Image\Facades\Image::class, 213 'ImageTool' => Intervention\Image\Facades\Image::class,
214 + 'PDF' => Barryvdh\DomPDF\Facade::class,
213 'Debugbar' => Barryvdh\Debugbar\Facade::class, 215 'Debugbar' => Barryvdh\Debugbar\Facade::class,
214 216
215 /** 217 /**
......
...@@ -161,6 +161,12 @@ form.search-box { ...@@ -161,6 +161,12 @@ form.search-box {
161 padding: $-xs 0; 161 padding: $-xs 0;
162 color: #555; 162 color: #555;
163 text-align: left !important; 163 text-align: left !important;
164 + &.wide {
165 + min-width: 220px;
166 + }
167 + .text-muted {
168 + color: #999;
169 + }
164 a { 170 a {
165 display: block; 171 display: block;
166 padding: $-xs $-m; 172 padding: $-xs $-m;
......
...@@ -27,8 +27,8 @@ $-xs: 6px; ...@@ -27,8 +27,8 @@ $-xs: 6px;
27 $-xxs: 3px; 27 $-xxs: 3px;
28 28
29 // Fonts 29 // Fonts
30 -$heading: 'Roboto', Helvetica, Arial, sans-serif; 30 +$heading: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
31 -$text: 'Roboto', Helvetica, Arial, sans-serif; 31 +$text: 'Roboto', 'DejaVu Sans', Helvetica, Arial, sans-serif;
32 $fs-m: 15px; 32 $fs-m: 15px;
33 $fs-s: 14px; 33 $fs-s: 14px;
34 34
......
1 -@import "reset"; 1 +//@import "reset";
2 @import "variables"; 2 @import "variables";
3 @import "mixins"; 3 @import "mixins";
4 @import "html"; 4 @import "html";
......
1 +<!doctype html>
2 +<html lang="en">
3 +<head>
4 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5 + <title>{{ $page->name }}</title>
6 +
7 + <style>
8 + {!! $css !!}
9 + </style>
10 + @yield('head')
11 +</head>
12 +<body>
13 +<div class="container" id="page-show">
14 + <div class="row">
15 + <div class="col-md-8 col-md-offset-2">
16 + <div class="page-content">
17 +
18 + @include('pages/page-display')
19 +
20 + <hr>
21 +
22 + <p class="text-muted small">
23 + Created {{$page->created_at->toDayDateTimeString()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
24 + <br>
25 + Last Updated {{$page->updated_at->toDayDateTimeString()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
26 + </p>
27 +
28 + </div>
29 + </div>
30 + </div>
31 +</div>
32 +</body>
33 +</html>
1 -<!doctype html> 1 +@extends('pages/export')
2 -<html lang="en">
3 -<head>
4 - <meta charset="UTF-8">
5 - <title>{{ $page->name }}</title>
6 2
3 +@section('head')
7 <style> 4 <style>
8 - {!! $css !!} 5 + body {
9 - </style> 6 + font-size: 15px;
10 -</head> 7 + line-height: 1;
11 -<body> 8 + }
12 -<div class="container" id="page-show" ng-non-bindable>
13 - <div class="row">
14 - <div class="col-md-8 col-md-offset-2">
15 - <div class="page-content">
16 -
17 - @include('pages/page-display')
18 9
19 - <hr> 10 + h1, h2, h3, h4, h5, h6 {
11 + line-height: 1;
12 + }
20 13
21 - <p class="text-muted small"> 14 + table {
22 - Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif 15 + max-width: 800px !important;
23 - <br> 16 + font-size: 0.8em;
24 - Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif 17 + width: auto !important;
25 - </p> 18 + }
26 19
27 - </div> 20 + table td {
28 - </div> 21 + width: auto !important;
29 - </div> 22 + }
30 -</div> 23 + </style>
31 -</body> 24 +@stop
32 -</html>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -20,9 +20,11 @@ ...@@ -20,9 +20,11 @@
20 <div class="col-sm-6 faded"> 20 <div class="col-sm-6 faded">
21 <div class="action-buttons"> 21 <div class="action-buttons">
22 <span dropdown class="dropdown-container"> 22 <span dropdown class="dropdown-container">
23 - <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export Page</div> 23 + <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
24 - <ul> 24 + <ul class="wide">
25 - <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained HTML File</a></li> 25 + <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
26 + <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
27 + <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
26 </ul> 28 </ul>
27 </span> 29 </span>
28 @if($currentUser->can('page-update')) 30 @if($currentUser->can('page-update'))
......
1 +*
2 +!.gitignore
...\ No newline at end of file ...\ No newline at end of file