namespace BookStack\Http\Controllers;
use Activity;
+use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
protected $pageRepo;
protected $bookRepo;
protected $chapterRepo;
+ protected $exportService;
/**
* PageController constructor.
- * @param PageRepo $pageRepo
- * @param BookRepo $bookRepo
- * @param ChapterRepo $chapterRepo
+ * @param PageRepo $pageRepo
+ * @param BookRepo $bookRepo
+ * @param ChapterRepo $chapterRepo
+ * @param ExportService $exportService
*/
- public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo)
+ public function __construct(PageRepo $pageRepo, BookRepo $bookRepo, ChapterRepo $chapterRepo, ExportService $exportService)
{
$this->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo;
+ $this->exportService = $exportService;
parent::__construct();
}
Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl());
}
+
+ public function exportPdf($bookSlug, $pageSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+ $cssContent = file_get_contents(public_path('/css/styles.css'));
+
+ return $pdf->download($pageSlug . '.pdf');
+ }
+
+ /**
+ * Export a page to a self-contained HTML file.
+ * @param $bookSlug
+ * @param $pageSlug
+ * @return \Illuminate\Http\Response
+ */
+ public function exportHtml($bookSlug, $pageSlug)
+ {
+ $book = $this->bookRepo->getBySlug($bookSlug);
+ $page = $this->pageRepo->getBySlug($pageSlug, $book->id);
+ $containedHtml = $this->exportService->pageToContainedHtml($page);
+ return response()->make($containedHtml, 200, [
+ 'Content-Type' => 'application/octet-stream',
+ 'Content-Disposition' => 'attachment; filename="'.$pageSlug.'.html'
+ ]);
+ }
}
Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort');
-
// Pages
Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show');
+ Route::get('/{bookSlug}/page/{pageSlug}/export/pdf', 'PageController@exportPdf');
+ Route::get('/{bookSlug}/page/{pageSlug}/export/html', 'PageController@exportHtml');
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');
Route::delete('/{bookSlug}/page/{pageSlug}', 'PageController@destroy');
- //Revisions
+ // Revisions
Route::get('/{bookSlug}/page/{pageSlug}/revisions', 'PageController@showRevisions');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}', 'PageController@showRevision');
Route::get('/{bookSlug}/page/{pageSlug}/revisions/{revId}/restore', 'PageController@restoreRevision');
--- /dev/null
+<?php namespace BookStack\Services;
+
+
+use BookStack\Page;
+
+class ExportService
+{
+
+
+ /**
+ * Convert a page to a self-contained HTML file.
+ * Includes required CSS & image content. Images are base64 encoded into the HTML.
+ * @param Page $page
+ * @return mixed|string
+ */
+ public function pageToContainedHtml(Page $page)
+ {
+ $cssContent = file_get_contents(public_path('/css/export-styles.css'));
+ $pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
+
+ $imageTagsOutput = [];
+ preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $pageHtml, $imageTagsOutput);
+
+ // Replace image src with base64 encoded image strings
+ if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
+ foreach ($imageTagsOutput[0] as $index => $imgMatch) {
+ $oldImgString = $imgMatch;
+ $srcString = $imageTagsOutput[2][$index];
+ if (strpos(trim($srcString), 'http') !== 0) {
+ $pathString = public_path($srcString);
+ } else {
+ $pathString = $srcString;
+ }
+ $imageContent = file_get_contents($pathString);
+ $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
+ $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
+ $pageHtml = str_replace($oldImgString, $newImageString, $pageHtml);
+ }
+ }
+
+ $linksOutput = [];
+ preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $pageHtml, $linksOutput);
+
+ // Replace image src with base64 encoded image strings
+ if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
+ foreach ($linksOutput[0] as $index => $linkMatch) {
+ $oldLinkString = $linkMatch;
+ $srcString = $linksOutput[2][$index];
+ if (strpos(trim($srcString), 'http') !== 0) {
+ $newSrcString = url($srcString);
+ $newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
+ $pageHtml = str_replace($oldLinkString, $newLinkString, $pageHtml);
+ }
+ }
+ }
+
+ // Replace any relative links with system domain
+ return $pageHtml;
+ }
+
+}
\ No newline at end of file
"packages": [
{
"name": "aws/aws-sdk-php",
- "version": "3.12.1",
+ "version": "3.13.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b"
+ "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b",
- "reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc1796d1c21146cdcbfb7628aee816acb7b85e09",
+ "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-spl": "*",
"nette/neon": "^2.3",
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "~4.0|~5.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"s3",
"sdk"
],
- "time": "2016-01-06 22:50:48"
+ "time": "2016-01-19 22:46:22"
},
{
"name": "barryvdh/laravel-debugbar",
},
{
"name": "laravel/framework",
- "version": "v5.2.7",
+ "version": "v5.2.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4"
+ "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4",
- "reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/93dc5b0089eef468157fd7200e575c3861ec59a5",
+ "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5",
"shasum": ""
},
"require": {
"framework",
"laravel"
],
- "time": "2016-01-07 13:54:34"
+ "time": "2016-01-13 20:29:10"
},
{
"name": "laravel/socialite",
},
{
"name": "symfony/class-loader",
- "version": "v2.8.1",
+ "version": "v2.8.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
- "reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c"
+ "reference": "98e9089a428ed0e39423b67352c57ef5910a3269"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/class-loader/zipball/ec74b0a279cf3a9bd36172b3e3061591d380ce6c",
- "reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c",
+ "url": "https://api.github.com/repos/symfony/class-loader/zipball/98e9089a428ed0e39423b67352c57ef5910a3269",
+ "reference": "98e9089a428ed0e39423b67352c57ef5910a3269",
"shasum": ""
},
"require": {
],
"description": "Symfony ClassLoader Component",
"homepage": "https://symfony.com",
- "time": "2015-12-05 17:37:59"
+ "time": "2016-01-03 15:33:41"
},
{
"name": "symfony/console",
elixir(function(mix) {
mix.sass('styles.scss')
.sass('print-styles.scss')
+ .sass('export-styles.scss')
.browserify('global.js', 'public/js/common.js')
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
});
--- /dev/null
+/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-bold-webfont.eot');
+ src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-bold-webfont.woff') format('woff'),
+ url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-bolditalic-webfont.eot');
+ src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
+ url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
+ font-weight: bold;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-italic-webfont.eot');
+ src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-italic-webfont.woff') format('woff'),
+ url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
+ font-weight: normal;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-light-webfont.eot');
+ src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-light-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-light-webfont.woff') format('woff'),
+ url('/fonts/roboto-light-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-lightitalic-webfont.eot');
+ src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
+ url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
+ font-weight: 300;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-medium-webfont.eot');
+ src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-medium-webfont.woff') format('woff'),
+ url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
+ font-weight: 500;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-mediumitalic-webfont.eot');
+ src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
+ url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
+ font-weight: 500;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Roboto';
+ src: url('/fonts/roboto-regular-webfont.eot');
+ src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
+ url('/fonts/roboto-regular-webfont.woff') format('woff'),
+ url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
+ url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
\ No newline at end of file
}
.faded {
- a, button, span {
+ a, button, span, span > div {
color: #666;
}
.text-button {
$bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);
-
-
-/* Generated by Font Squirrel (http://www.fontsquirrel.com) on December 30, 2015 */
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-bold-webfont.eot');
- src: url('/fonts/roboto-bold-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-bold-webfont.woff2') format('woff2'),
- url('/fonts/roboto-bold-webfont.woff') format('woff'),
- url('/fonts/roboto-bold-webfont.ttf') format('truetype'),
- url('/fonts/roboto-bold-webfont.svg#robotobold') format('svg');
- font-weight: bold;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-bolditalic-webfont.eot');
- src: url('/fonts/roboto-bolditalic-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-bolditalic-webfont.woff2') format('woff2'),
- url('/fonts/roboto-bolditalic-webfont.woff') format('woff'),
- url('/fonts/roboto-bolditalic-webfont.ttf') format('truetype'),
- url('/fonts/roboto-bolditalic-webfont.svg#robotobold_italic') format('svg');
- font-weight: bold;
- font-style: italic;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-italic-webfont.eot');
- src: url('/fonts/roboto-italic-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-italic-webfont.woff2') format('woff2'),
- url('/fonts/roboto-italic-webfont.woff') format('woff'),
- url('/fonts/roboto-italic-webfont.ttf') format('truetype'),
- url('/fonts/roboto-italic-webfont.svg#robotoitalic') format('svg');
- font-weight: normal;
- font-style: italic;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-light-webfont.eot');
- src: url('/fonts/roboto-light-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-light-webfont.woff2') format('woff2'),
- url('/fonts/roboto-light-webfont.woff') format('woff'),
- url('/fonts/roboto-light-webfont.ttf') format('truetype'),
- url('/fonts/roboto-light-webfont.svg#robotolight') format('svg');
- font-weight: 300;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-lightitalic-webfont.eot');
- src: url('/fonts/roboto-lightitalic-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-lightitalic-webfont.woff2') format('woff2'),
- url('/fonts/roboto-lightitalic-webfont.woff') format('woff'),
- url('/fonts/roboto-lightitalic-webfont.ttf') format('truetype'),
- url('/fonts/roboto-lightitalic-webfont.svg#robotolight_italic') format('svg');
- font-weight: 300;
- font-style: italic;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-medium-webfont.eot');
- src: url('/fonts/roboto-medium-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-medium-webfont.woff2') format('woff2'),
- url('/fonts/roboto-medium-webfont.woff') format('woff'),
- url('/fonts/roboto-medium-webfont.ttf') format('truetype'),
- url('/fonts/roboto-medium-webfont.svg#robotomedium') format('svg');
- font-weight: 500;
- font-style: normal;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-mediumitalic-webfont.eot');
- src: url('/fonts/roboto-mediumitalic-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-mediumitalic-webfont.woff2') format('woff2'),
- url('/fonts/roboto-mediumitalic-webfont.woff') format('woff'),
- url('/fonts/roboto-mediumitalic-webfont.ttf') format('truetype'),
- url('/fonts/roboto-mediumitalic-webfont.svg#robotomedium_italic') format('svg');
- font-weight: 500;
- font-style: italic;
-}
-
-@font-face {
- font-family: 'Roboto';
- src: url('/fonts/roboto-regular-webfont.eot');
- src: url('/fonts/roboto-regular-webfont.eot?#iefix') format('embedded-opentype'),
- url('/fonts/roboto-regular-webfont.woff2') format('woff2'),
- url('/fonts/roboto-regular-webfont.woff') format('woff'),
- url('/fonts/roboto-regular-webfont.ttf') format('truetype'),
- url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg');
- font-weight: normal;
- font-style: normal;
-}
--- /dev/null
+@import "reset";
+@import "variables";
+@import "mixins";
+@import "html";
+@import "text";
+@import "grid";
+@import "blocks";
+@import "forms";
+@import "tables";
+@import "header";
+@import "lists";
+@import "pages";
\ No newline at end of file
@import "reset";
@import "variables";
+@import "fonts";
@import "mixins";
@import "html";
@import "text";
--- /dev/null
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>{{ $page->name }}</title>
+
+ <style>
+ {!! $css !!}
+ </style>
+</head>
+<body>
+<div class="container" id="page-show" ng-non-bindable>
+ <div class="row">
+ <div class="col-md-8 col-md-offset-2">
+ <div class="page-content">
+
+ @include('pages/page-display')
+
+ <hr>
+
+ <p class="text-muted small">
+ Created {{$page->created_at->diffForHumans()}} @if($page->createdBy) by {{$page->createdBy->name}} @endif
+ <br>
+ Last Updated {{$page->updated_at->diffForHumans()}} @if($page->updatedBy) by {{$page->updatedBy->name}} @endif
+ </p>
+
+ </div>
+ </div>
+ </div>
+</div>
+</body>
+</html>
</div>
<div class="col-sm-6 faded">
<div class="action-buttons">
+ <span dropdown class="dropdown-container">
+ <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export Page</div>
+ <ul>
+ <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained HTML File</a></li>
+ </ul>
+ </span>
@if($currentUser->can('page-update'))
<a href="{{$page->getUrl() . '/revisions'}}" class="text-primary text-button"><i class="zmdi zmdi-replay"></i>Revisions</a>
<a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>