diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index e2b10d3d3..c3d8e396c 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -164,6 +164,7 @@ class PageController extends Controller $draft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id); $page->name = $draft->name; $page->html = $draft->html; + $page->markdown = $draft->markdown; $page->isDraft = true; $warnings [] = $this->pageRepo->getUserPageDraftMessage($draft); } @@ -204,9 +205,9 @@ class PageController extends Controller $page = $this->pageRepo->getById($pageId, true); $this->checkOwnablePermission('page-update', $page); if ($page->draft) { - $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html'])); + $draft = $this->pageRepo->updateDraftPage($page, $request->only(['name', 'html', 'markdown'])); } else { - $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html'])); + $draft = $this->pageRepo->saveUpdateDraft($page, $request->only(['name', 'html', 'markdown'])); } $updateTime = $draft->updated_at->format('H:i'); return response()->json(['status' => 'success', 'message' => 'Draft saved at ' . $updateTime]); diff --git a/app/Page.php b/app/Page.php index 84e37d519..d2a303f61 100644 --- a/app/Page.php +++ b/app/Page.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Model; class Page extends Entity { - protected $fillable = ['name', 'html', 'priority']; + protected $fillable = ['name', 'html', 'priority', 'markdown']; protected $simpleAttributes = ['name', 'id', 'slug']; diff --git a/app/PageRevision.php b/app/PageRevision.php index f1b4bc587..c258913ff 100644 --- a/app/PageRevision.php +++ b/app/PageRevision.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Model; class PageRevision extends Model { - protected $fillable = ['name', 'html', 'text']; + protected $fillable = ['name', 'html', 'text', 'markdown']; /** * Get the user that created the page revision diff --git a/config/app.php b/config/app.php index 650ad1d07..d305af3c0 100644 --- a/config/app.php +++ b/config/app.php @@ -5,6 +5,8 @@ return [ 'env' => env('APP_ENV', 'production'), + 'editor' => env('APP_EDITOR', 'html'), + /* |-------------------------------------------------------------------------- | Application Debug Mode diff --git a/database/migrations/2016_03_25_123157_add_markdown_support.php b/database/migrations/2016_03_25_123157_add_markdown_support.php new file mode 100644 index 000000000..45efe5a09 --- /dev/null +++ b/database/migrations/2016_03_25_123157_add_markdown_support.php @@ -0,0 +1,39 @@ +longText('markdown'); + }); + + Schema::table('page_revisions', function (Blueprint $table) { + $table->longText('markdown'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('pages', function (Blueprint $table) { + $table->dropColumn('markdown'); + }); + + Schema::table('page_revisions', function (Blueprint $table) { + $table->dropColumn('markdown'); + }); + } +} diff --git a/package.json b/package.json index a1fb06b1c..7d1aa1a6a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "bootstrap-sass": "^3.0.0", "dropzone": "^4.0.1", "laravel-elixir": "^3.4.0", + "marked": "^0.3.5", "zeroclipboard": "^2.2.0" } } diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff new file mode 100644 index 000000000..8cb9e6fd8 Binary files /dev/null and b/public/fonts/roboto-mono-v4-latin-regular.woff differ diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2 new file mode 100644 index 000000000..1f6598111 Binary files /dev/null and b/public/fonts/roboto-mono-v4-latin-regular.woff2 differ diff --git a/readme.md b/readme.md index 0730e3de3..93296767f 100644 --- a/readme.md +++ b/readme.md @@ -176,3 +176,4 @@ These are the great projects used to help build BookStack: * [Dropzone.js](http://www.dropzonejs.com/) * [ZeroClipboard](http://zeroclipboard.org/) * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) +* [Marked](https://github.com/chjj/marked) diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index 29a448265..09187c0c2 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -216,16 +216,20 @@ module.exports = function (ngApp, events) { }]); - ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', function ($scope, $http, $attrs, $interval, $timeout) { + ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', + function ($scope, $http, $attrs, $interval, $timeout, $sce) { $scope.editorOptions = require('./pages/page-form'); - $scope.editorHtml = ''; + $scope.editContent = ''; $scope.draftText = ''; var pageId = Number($attrs.pageId); var isEdit = pageId !== 0; var autosaveFrequency = 30; // AutoSave interval in seconds. + var isMarkdown = $attrs.editorType === 'markdown'; $scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1; $scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1; + + // Set inital header draft text if ($scope.isUpdateDraft || $scope.isNewPageDraft) { $scope.draftText = 'Editing Draft' } else { @@ -245,7 +249,14 @@ module.exports = function (ngApp, events) { }, 1000); } - $scope.editorChange = function () {} + // Actions specifically for the markdown editor + if (isMarkdown) { + $scope.displayContent = ''; + // Editor change event + $scope.editorChange = function (content) { + $scope.displayContent = $sce.trustAsHtml(content); + } + } /** * Start the AutoSave loop, Checks for content change @@ -253,17 +264,18 @@ module.exports = function (ngApp, events) { */ function startAutoSave() { currentContent.title = $('#name').val(); - currentContent.html = $scope.editorHtml; + currentContent.html = $scope.editContent; autoSave = $interval(() => { var newTitle = $('#name').val(); - var newHtml = $scope.editorHtml; + var newHtml = $scope.editContent; if (newTitle !== currentContent.title || newHtml !== currentContent.html) { currentContent.html = newHtml; currentContent.title = newTitle; - saveDraft(newTitle, newHtml); + saveDraft(); } + }, 1000 * autosaveFrequency); } @@ -272,20 +284,24 @@ module.exports = function (ngApp, events) { * @param title * @param html */ - function saveDraft(title, html) { - $http.put('/ajax/page/' + pageId + '/save-draft', { - name: title, - html: html - }).then((responseData) => { + function saveDraft() { + var data = { + name: $('#name').val(), + html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent + }; + + if (isMarkdown) data.markdown = $scope.editContent; + + console.log(data.markdown); + + $http.put('/ajax/page/' + pageId + '/save-draft', data).then((responseData) => { $scope.draftText = responseData.data.message; if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true; }); } $scope.forceDraftSave = function() { - var newTitle = $('#name').val(); - var newHtml = $scope.editorHtml; - saveDraft(newTitle, newHtml); + saveDraft(); }; /** @@ -298,6 +314,7 @@ module.exports = function (ngApp, events) { $scope.draftText = 'Editing Page'; $scope.isUpdateDraft = false; $scope.$broadcast('html-update', responseData.data.html); + $scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html); $('#name').val(responseData.data.name); $timeout(() => { startAutoSave(); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 71b35fb42..316e5dcb4 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,5 +1,6 @@ "use strict"; var DropZone = require('dropzone'); +var markdown = require( "marked" ); var toggleSwitchTemplate = require('./components/toggle-switch.html'); var imagePickerTemplate = require('./components/image-picker.html'); @@ -202,5 +203,36 @@ module.exports = function (ngApp, events) { } }]) + ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + return { + restrict: 'A', + scope: { + mdModel: '=', + mdChange: '=' + }, + link: function (scope, element, attrs) { + + // Set initial model content + var content = element.val(); + scope.mdModel = content; + scope.mdChange(markdown(content)); + + element.on('change input', (e) => { + content = element.val(); + $timeout(() => { + scope.mdModel = content; + scope.mdChange(markdown(content)); + }); + }); + + scope.$on('markdown-update', (event, value) => { + element.val(value); + scope.mdModel= value; + scope.mdChange(markdown(value)); + }); + + } + } + }]) }; \ No newline at end of file diff --git a/resources/assets/sass/_fonts.scss b/resources/assets/sass/_fonts.scss index 0dc8c95b2..8cf677779 100644 --- a/resources/assets/sass/_fonts.scss +++ b/resources/assets/sass/_fonts.scss @@ -93,4 +93,15 @@ url('/fonts/roboto-regular-webfont.svg#robotoregular') format('svg'); font-weight: normal; font-style: normal; +} + +/* roboto-mono-regular - latin */ +// https://google-webfonts-helper.herokuapp.com +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + src: local('Roboto Mono'), local('RobotoMono-Regular'), + url('/fonts/roboto-mono-v4-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+ */ + url('/fonts/roboto-mono-v4-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ } \ No newline at end of file diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index 037dad94a..5351f06e7 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -26,6 +26,58 @@ display: none; } +#markdown-editor { + position: relative; + z-index: 5; + textarea { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + padding: $-xs $-m; + color: #444; + border-radius: 0; + max-height: 100%; + flex: 1; + border: 0; + &:focus { + outline: 0; + } + } + .markdown-display, .markdown-editor-wrap { + flex: 1; + padding-top: 28px; + position: relative; + border: 1px solid #DDD; + &:before { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: $-xs $-m; + font-family: 'Roboto Mono'; + font-size: 11px; + line-height: 1; + border-bottom: 1px solid #DDD; + background-color: #EEE; + } + } + .markdown-editor-wrap { + display: flex; + &:before { + content: 'Editor'; + } + } + .markdown-display { + padding: 0 $-m; + padding-top: 28px; + margin-left: -1px; + &:before { + content: 'Preview'; + } + } +} + label { display: block; line-height: 1.4em; @@ -160,6 +212,10 @@ input:checked + .toggle-switch { width: 100%; } +div[editor-type="markdown"] .title-input.page-title input[type="text"] { + max-width: 100%; +} + .search-box { max-width: 100%; position: relative; diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 721eeb238..1a55cf868 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -157,6 +157,12 @@ span.code { @extend .code-base; padding: 1px $-xs; } + +pre code { + background-color: transparent; + border: 0; + font-size: 1em; +} /* * Text colors */ diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php index d8dc19ec2..2118d23b2 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,5 +1,5 @@ -