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/app/Repos/PageRepo.php b/app/Repos/PageRepo.php index 4c3512fa7..9a7502754 100644 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@ -312,6 +312,7 @@ class PageRepo extends EntityRepo $page->fill($input); $page->html = $this->formatHtml($input['html']); $page->text = strip_tags($page->html); + if (setting('app-editor') !== 'markdown') $page->markdown = ''; $page->updated_by = $userId; $page->save(); @@ -348,6 +349,7 @@ class PageRepo extends EntityRepo public function saveRevision(Page $page) { $revision = $this->pageRevision->fill($page->toArray()); + if (setting('app-editor') !== 'markdown') $revision->markdown = ''; $revision->page_id = $page->id; $revision->slug = $page->slug; $revision->book_slug = $page->book->slug; @@ -386,6 +388,8 @@ class PageRepo extends EntityRepo } $draft->fill($data); + if (setting('app-editor') !== 'markdown') $draft->markdown = ''; + $draft->save(); return $draft; } diff --git a/app/Services/SettingService.php b/app/Services/SettingService.php index bcc7eae31..bf5fa918e 100644 --- a/app/Services/SettingService.php +++ b/app/Services/SettingService.php @@ -44,28 +44,39 @@ class SettingService /** * Gets a setting value from the cache or database. + * Looks at the system defaults if not cached or in database. * @param $key * @param $default * @return mixed */ protected function getValueFromStore($key, $default) { + // Check for an overriding value $overrideValue = $this->getOverrideValue($key); if ($overrideValue !== null) return $overrideValue; + // Check the cache $cacheKey = $this->cachePrefix . $key; if ($this->cache->has($cacheKey)) { return $this->cache->get($cacheKey); } + // Check the database $settingObject = $this->getSettingObjectByKey($key); - if ($settingObject !== null) { $value = $settingObject->value; $this->cache->forever($cacheKey, $value); return $value; } + // Check the defaults set in the app config. + $configPrefix = 'setting-defaults.' . $key; + if (config()->has($configPrefix)) { + $value = config($configPrefix); + $this->cache->forever($cacheKey, $value); + return $value; + } + return $default; } 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/config/setting-defaults.php b/config/setting-defaults.php new file mode 100644 index 000000000..17bae1848 --- /dev/null +++ b/config/setting-defaults.php @@ -0,0 +1,10 @@ + 'wysiwyg' + +]; \ No newline at end of file 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..2daa32cfb --- /dev/null +++ b/database/migrations/2016_03_25_123157_add_markdown_support.php @@ -0,0 +1,39 @@ +longText('markdown')->default(''); + }); + + Schema::table('page_revisions', function (Blueprint $table) { + $table->longText('markdown')->default(''); + }); + } + + /** + * 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 62893aea3..067983e84 100644 --- a/readme.md +++ b/readme.md @@ -45,3 +45,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..dbd2e1ae6 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,18 @@ 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); + } + } + + if (!isMarkdown) { + $scope.editorChange = function() {}; + } /** * Start the AutoSave loop, Checks for content change @@ -253,17 +268,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 +288,22 @@ 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; + + $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 +316,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..de87950dc 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'); @@ -200,7 +201,82 @@ module.exports = function (ngApp, events) { tinymce.init(scope.tinymce); } } + }]); + + ngApp.directive('markdownInput', ['$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)); + }); + + } + } + }]); + + ngApp.directive('markdownEditor', ['$timeout', function($timeout) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + + // Elements + var input = element.find('textarea[markdown-input]'); + var insertImage = element.find('button[data-action="insertImage"]'); + + var currentCaretPos = 0; + + input.blur((event) => { + currentCaretPos = input[0].selectionStart; + }); + + // Insert image shortcut + input.keydown((event) => { + if (event.which === 73 && event.ctrlKey && event.shiftKey) { + event.preventDefault(); + var caretPos = input[0].selectionStart; + var currentContent = input.val(); + var mdImageText = ""; + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); + input.focus(); + input[0].selectionStart = caretPos + ("; + input[0].selectionEnd = caretPos + ('; + } + }); + + // Insert image from image manager + insertImage.click((event) => { + window.ImageManager.showExternal((image) => { + var caretPos = currentCaretPos; + var currentContent = input.val(); + var mdImageText = ""; + input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); + input.change(); + }); + }); + + } + } }]) - }; \ 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..4a505c5f8 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -26,6 +26,59 @@ 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; + width: 100%; + &:focus { + outline: 0; + } + } + .markdown-display, .markdown-editor-wrap { + flex: 1; + position: relative; + } + .markdown-editor-wrap { + display: flex; + flex-direction: column; + border: 1px solid #DDD; + } + .markdown-display { + padding: 0 $-m 0; + margin-left: -1px; + overflow-y: scroll; + .page-content { + margin: 0 auto; + } + } +} +.editor-toolbar { + width: 100%; + padding: $-xs $-m; + font-family: 'Roboto Mono'; + font-size: 11px; + line-height: 1.6; + border-bottom: 1px solid #DDD; + background-color: #EEE; + flex: none; + &:after { + content: ''; + display: block; + clear: both; + } +} + + label { display: block; line-height: 1.4em; @@ -160,6 +213,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..7ce9dbfe5 100644 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@ -1,5 +1,5 @@ -
For performance reasons, all images are public by default, This option adds a random, hard-to-guess characters in front of image names. Ensure directory indexes are not enabled to prevent easy access.
-Select which editor will be used by all users to edit pages.
+This image should be 43px in height.
Large images will be scaled down.
This should be a hex value.
Leave empty to reset to the default color.
Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application.
Note that users will be able to change their email addresses after successful registration.