Added page HTML export

This commit is contained in:
Dan Brown 2016-01-20 22:13:13 +00:00
parent 7bcd967fd9
commit ea2e16cabb
12 changed files with 263 additions and 121 deletions

View File

@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers; namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Services\ExportService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -18,18 +19,21 @@ class PageController extends Controller
protected $pageRepo; protected $pageRepo;
protected $bookRepo; protected $bookRepo;
protected $chapterRepo; protected $chapterRepo;
protected $exportService;
/** /**
* PageController constructor. * PageController constructor.
* @param PageRepo $pageRepo * @param PageRepo $pageRepo
* @param BookRepo $bookRepo * @param BookRepo $bookRepo
* @param ChapterRepo $chapterRepo * @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->pageRepo = $pageRepo;
$this->bookRepo = $bookRepo; $this->bookRepo = $bookRepo;
$this->chapterRepo = $chapterRepo; $this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
parent::__construct(); parent::__construct();
} }
@ -221,4 +225,30 @@ class PageController extends Controller
Activity::add($page, 'page_restore', $book->id); Activity::add($page, 'page_restore', $book->id);
return redirect($page->getUrl()); 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'
]);
}
} }

View File

@ -18,11 +18,12 @@ Route::group(['middleware' => 'auth'], function () {
Route::get('/{bookSlug}/sort', 'BookController@sort'); Route::get('/{bookSlug}/sort', 'BookController@sort');
Route::put('/{bookSlug}/sort', 'BookController@saveSort'); Route::put('/{bookSlug}/sort', 'BookController@saveSort');
// Pages // Pages
Route::get('/{bookSlug}/page/create', 'PageController@create'); Route::get('/{bookSlug}/page/create', 'PageController@create');
Route::post('/{bookSlug}/page', 'PageController@store'); Route::post('/{bookSlug}/page', 'PageController@store');
Route::get('/{bookSlug}/page/{pageSlug}', 'PageController@show'); 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}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete'); Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update'); Route::put('/{bookSlug}/page/{pageSlug}', 'PageController@update');

View File

@ -0,0 +1,61 @@
<?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;
}
}

32
composer.lock generated
View File

@ -9,16 +9,16 @@
"packages": [ "packages": [
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.12.1", "version": "3.13.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b" "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cc1796d1c21146cdcbfb7628aee816acb7b85e09",
"reference": "5ee0f33fafe47740c03ff38ddb73ae4f52b4da5b", "reference": "cc1796d1c21146cdcbfb7628aee816acb7b85e09",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -40,7 +40,7 @@
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-spl": "*", "ext-spl": "*",
"nette/neon": "^2.3", "nette/neon": "^2.3",
"phpunit/phpunit": "~4.0" "phpunit/phpunit": "~4.0|~5.0"
}, },
"suggest": { "suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
@ -84,7 +84,7 @@
"s3", "s3",
"sdk" "sdk"
], ],
"time": "2016-01-06 22:50:48" "time": "2016-01-19 22:46:22"
}, },
{ {
"name": "barryvdh/laravel-debugbar", "name": "barryvdh/laravel-debugbar",
@ -829,16 +829,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v5.2.7", "version": "v5.2.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4" "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4", "url": "https://api.github.com/repos/laravel/framework/zipball/93dc5b0089eef468157fd7200e575c3861ec59a5",
"reference": "26cd65eaa4bcc0fb0be381cfb7cfdcda06a3c2b4", "reference": "93dc5b0089eef468157fd7200e575c3861ec59a5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -953,7 +953,7 @@
"framework", "framework",
"laravel" "laravel"
], ],
"time": "2016-01-07 13:54:34" "time": "2016-01-13 20:29:10"
}, },
{ {
"name": "laravel/socialite", "name": "laravel/socialite",
@ -1849,16 +1849,16 @@
}, },
{ {
"name": "symfony/class-loader", "name": "symfony/class-loader",
"version": "v2.8.1", "version": "v2.8.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/class-loader.git", "url": "https://github.com/symfony/class-loader.git",
"reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c" "reference": "98e9089a428ed0e39423b67352c57ef5910a3269"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/class-loader/zipball/ec74b0a279cf3a9bd36172b3e3061591d380ce6c", "url": "https://api.github.com/repos/symfony/class-loader/zipball/98e9089a428ed0e39423b67352c57ef5910a3269",
"reference": "ec74b0a279cf3a9bd36172b3e3061591d380ce6c", "reference": "98e9089a428ed0e39423b67352c57ef5910a3269",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1897,7 +1897,7 @@
], ],
"description": "Symfony ClassLoader Component", "description": "Symfony ClassLoader Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2015-12-05 17:37:59" "time": "2016-01-03 15:33:41"
}, },
{ {
"name": "symfony/console", "name": "symfony/console",

View File

@ -21,6 +21,7 @@ elixir.extend('queryVersion', function(inputFiles) {
elixir(function(mix) { elixir(function(mix) {
mix.sass('styles.scss') mix.sass('styles.scss')
.sass('print-styles.scss') .sass('print-styles.scss')
.sass('export-styles.scss')
.browserify('global.js', 'public/js/common.js') .browserify('global.js', 'public/js/common.js')
.queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']);
}); });

View File

@ -0,0 +1,96 @@
/* 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;
}

View File

@ -187,7 +187,7 @@ form.search-box {
} }
.faded { .faded {
a, button, span { a, button, span, span > div {
color: #666; color: #666;
} }
.text-button { .text-button {

View File

@ -52,101 +52,3 @@ $text-light: #EEE;
$bs-light: 0 0 4px 1px #CCC; $bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); $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;
}

View File

@ -0,0 +1,12 @@
@import "reset";
@import "variables";
@import "mixins";
@import "html";
@import "text";
@import "grid";
@import "blocks";
@import "forms";
@import "tables";
@import "header";
@import "lists";
@import "pages";

View File

@ -1,5 +1,6 @@
@import "reset"; @import "reset";
@import "variables"; @import "variables";
@import "fonts";
@import "mixins"; @import "mixins";
@import "html"; @import "html";
@import "text"; @import "text";

View File

@ -0,0 +1,32 @@
<!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>

View File

@ -19,6 +19,12 @@
</div> </div>
<div class="col-sm-6 faded"> <div class="col-sm-6 faded">
<div class="action-buttons"> <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')) @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() . '/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> <a href="{{$page->getUrl() . '/edit'}}" class="text-primary text-button" ><i class="zmdi zmdi-edit"></i>Edit</a>