Caching + Edit Mode UI

This commit is contained in:
NGPixel 2016-08-27 21:46:10 -04:00
parent 1d2893765c
commit 4be54310c4
18 changed files with 330 additions and 68 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
"use strict";function _classCallCheck(e,s){if(!(e instanceof s))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,s){for(var t=0;t<s.length;t++){var n=s[t];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(s,t,n){return t&&e(s.prototype,t),n&&e(s,n),s}}(),Alerts=function(){function e(){_classCallCheck(this,e);var s=this;s.mdl=new Vue({el:"#alerts",data:{children:[]},methods:{acknowledge:function(e){s.close(e)}}}),s.uidNext=1}return _createClass(e,[{key:"push",value:function(e){var s=this,t=_.defaults(e,{_uid:s.uidNext,class:"is-info",message:"---",sticky:!1,title:"---"});s.mdl.children.push(t),t.sticky||_.delay(function(){s.close(t._uid)},5e3),s.uidNext++}},{key:"pushError",value:function(e,s){this.push({class:"is-danger",message:s,sticky:!1,title:e})}},{key:"pushSuccess",value:function(e,s){this.push({class:"is-success",message:s,sticky:!1,title:e})}},{key:"close",value:function(e){var s=this,t=_.findIndex(s.mdl.children,["_uid",e]),n=_.nth(s.mdl.children,t);t>=0&&n&&(n.class+=" exit",s.mdl.children.$set(t,n),_.delay(function(){s.mdl.children.$remove(n)},500))}}]),e}();jQuery(document).ready(function(e){e("a").smoothScroll({speed:400,offset:-20});var s=(new Sticky(".stickyscroll"),new Alerts);alertsData&&_.forEach(alertsData,function(e){s.push(e)})});
"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var _createClass=function(){function e(e,t){for(var n=0;n<t.length;n++){var s=t[n];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}return function(t,n,s){return n&&e(t.prototype,n),s&&e(t,s),t}}(),Alerts=function(){function e(){_classCallCheck(this,e);var t=this;t.mdl=new Vue({el:"#alerts",data:{children:[]},methods:{acknowledge:function(e){t.close(e)}}}),t.uidNext=1}return _createClass(e,[{key:"push",value:function(e){var t=this,n=_.defaults(e,{_uid:t.uidNext,class:"is-info",message:"---",sticky:!1,title:"---"});t.mdl.children.push(n),n.sticky||_.delay(function(){t.close(n._uid)},5e3),t.uidNext++}},{key:"pushError",value:function(e,t){this.push({class:"is-danger",message:t,sticky:!1,title:e})}},{key:"pushSuccess",value:function(e,t){this.push({class:"is-success",message:t,sticky:!1,title:e})}},{key:"close",value:function(e){var t=this,n=_.findIndex(t.mdl.children,["_uid",e]),s=_.nth(t.mdl.children,n);n>=0&&s&&(s.class+=" exit",t.mdl.children.$set(n,s),_.delay(function(){t.mdl.children.$remove(s)},500))}}]),e}();jQuery(document).ready(function(e){e("a").smoothScroll({speed:400,offset:-20});var t=(new Sticky(".stickyscroll"),new Alerts);if(alertsData&&_.forEach(alertsData,function(e){t.push(e)}),1===e("#mk-editor").length){new SimpleMDE({autofocus:!0,element:e("#mk-editor").get(0),autoDownloadFontAwesome:!1,placeholder:"Enter Markdown formatted content here...",hideIcons:["heading","quote"],showIcons:["strikethrough","heading-1","heading-2","heading-3","code","table","horizontal-rule"],spellChecker:!1})}});

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,8 @@
jQuery( document ).ready(function( $ ) {
// Scroll
$('a').smoothScroll({
speed: 400,
offset: -20
@ -9,6 +11,8 @@ jQuery( document ).ready(function( $ ) {
var sticky = new Sticky('.stickyscroll');
// Alerts
var alerts = new Alerts();
if(alertsData) {
_.forEach(alertsData, (alertRow) => {
@ -16,4 +20,20 @@ jQuery( document ).ready(function( $ ) {
});
}
// Editor
if($('#mk-editor').length === 1) {
let mde = new SimpleMDE({
autofocus: true,
element: $("#mk-editor").get(0),
autoDownloadFontAwesome: false,
placeholder: 'Enter Markdown formatted content here...',
hideIcons: ['heading', 'quote'],
showIcons: ['strikethrough', 'heading-1', 'heading-2', 'heading-3', 'code', 'table', 'horizontal-rule'],
spellChecker: false
});
}
});

View File

@ -1,13 +1,20 @@
//@import './layout/_fonts';
@import './layout/_base';
$warning: #f68b39;
$red: #E53935;
$orange: #FB8C00;
$blue: #039BE5;
$turquoise: #00ACC1;
$green: #7CB342;
$warning: $orange;
@import 'bulma';
@import './libs/twemoji-awesome';
@import './libs/animate.min.css';
@import './components/_alerts';
@import './components/_editor';
@import './layout/_header';
@import './layout/_footer';

View File

@ -0,0 +1,8 @@
.editor-toolbar i.separator {
margin-top: 5px;
}
.editor-toolbar .fa {
font-size: 14px;
}

View File

@ -6,6 +6,10 @@
}
.section.is-small {
padding: 20px 20px;
}
.mkcontent {
h1 {
@ -26,12 +30,31 @@
}
.hljs {
a.external-link {
position: relative;
padding-left: 20px;
&:before {
content: "\f08e";
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
text-decoration: inherit;
color: $grey;
font-size: 14px;
position: absolute;
top: 0;
left: 0;
}
}
pre {
padding: 0;
border-radius: 3px;
> code {
box-shadow: inset 0 0 5px 0 $grey-light;
border-radius: 5px;
}
}
@ -54,6 +77,10 @@
color: $grey-dark;
}
.twa {
font-size: 120%;
}
}
.content a:not(.button):visited {

View File

@ -0,0 +1,5 @@
h2.nav-item {
font-size: 150%;
color: $orange;
}

View File

@ -2,9 +2,32 @@
var express = require('express');
var router = express.Router();
var _ = require('lodash');
router.get('/edit/*', (req, res, next) => {
res.send('EDIT MODE');
let safePath = entries.parsePath(_.replace(req.path, '/edit', ''));
entries.fetchOriginal(safePath, {
parseMarkdown: false,
parseMeta: true,
parseTree: false,
includeMarkdown: true,
includeParentInfo: false,
cache: false
}).then((pageData) => {
if(pageData) {
return res.render('pages/edit', { pageData });
} else {
throw new Error('Invalid page path.');
}
}).catch((err) => {
res.render('error', {
message: err.message,
error: {}
});
});
});
router.get('/new/*', (req, res, next) => {
@ -19,13 +42,13 @@ router.get('/*', (req, res, next) => {
let safePath = entries.parsePath(req.path);
entries.fetch(safePath).then((pageData) => {
console.log(pageData);
if(pageData) {
res.render('pages/view', { pageData });
return res.render('pages/view', { pageData });
} else {
next();
return next();
}
}).catch((err) => {
winston.error(err);
next();
});

View File

@ -23,7 +23,8 @@ var paths = {
'./node_modules/jquery/dist/jquery.min.js',
'./node_modules/vue/dist/vue.min.js',
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.min.js',
'./node_modules/sticky-js/dist/sticky.min.js'
'./node_modules/sticky-js/dist/sticky.min.js',
'./node_modules/simplemde/dist/simplemde.min.js'
],
scriptapps: [
'./client/js/components/*.js',
@ -34,7 +35,8 @@ var paths = {
],
csslibs: [
'./node_modules/font-awesome/css/font-awesome.min.css',
'./node_modules/highlight.js/styles/default.css'
'./node_modules/highlight.js/styles/default.css',
'./node_modules/simplemde/dist/simplemde.min.css'
],
cssapps: [
'./client/scss/app.scss'

View File

@ -5,7 +5,8 @@ var Promise = require('bluebird'),
fs = Promise.promisifyAll(require("fs")),
_ = require('lodash'),
farmhash = require('farmhash'),
msgpack = require('msgpack5')();
BSONModule = require('bson'),
BSON = new BSONModule.BSONPure.BSON();
/**
* Entries Model
@ -32,12 +33,17 @@ module.exports = {
},
/**
* Fetch an entry from cache, otherwise the original
*
* @param {String} entryPath The entry path
* @return {Object} Page Data
*/
fetch(entryPath) {
let self = this;
let fpath = path.join(self._repoPath, entryPath + '.md');
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bin');
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
return fs.statAsync(cpath).then((st) => {
return st.isFile();
@ -47,10 +53,10 @@ module.exports = {
if(isCache) {
console.log('from cache!');
// Load from cache
return fs.readFileAsync(cpath, 'utf8').then((contents) => {
return msgpack.decode(contents);
return fs.readFileAsync(cpath).then((contents) => {
return BSON.deserialize(contents);
}).catch((err) => {
winston.error('Corrupted cache file. Deleting it...');
fs.unlinkSync(cpath);
@ -59,38 +65,96 @@ module.exports = {
} else {
console.log('original!');
// Load original
// Parse original and cache it
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageData = mark.parse(contents);
if(!pageData.meta.title) {
pageData.meta.title = entryPath;
}
let cacheData = msgpack.encode(pageData);
return fs.writeFileAsync(cpath, cacheData, { encoding: 'utf8' }).then(() => {
return pageData;
}).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.');
return pageData;
});
});
} else {
return false;
}
});
return self.fetchOriginal(entryPath);
}
});
},
/**
* Fetches the original document entry
*
* @param {String} entryPath The entry path
* @param {Object} options The options
* @return {Object} Page data
*/
fetchOriginal(entryPath, options) {
let self = this;
let fpath = path.join(self._repoPath, entryPath + '.md');
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
options = _.defaults(options, {
parseMarkdown: true,
parseMeta: true,
parseTree: true,
includeMarkdown: false,
includeParentInfo: true,
cache: true
});
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
// Parse contents
let pageData = {
markdown: (options.includeMarkdown) ? contents : '',
html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
tree: (options.parseTree) ? mark.parseTree(contents) : []
};
if(!pageData.meta.title) {
pageData.meta.title = _.startCase(entryPath);
}
pageData.meta.path = entryPath;
// Get parent
let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
return (pageData.parent = parentData);
}).catch((err) => {
return (pageData.parent = false);
}) : Promise.resolve(true);
return parentPromise.then(() => {
// Cache to disk
if(options.cache) {
let cacheData = BSON.serialize(pageData, false, false, false);
return fs.writeFileAsync(cpath, cacheData).catch((err) => {
winston.error('Unable to write to cache! Performance may be affected.');
return true;
});
} else {
return true;
}
}).return(pageData);
});
} else {
return false;
}
});
},
/**
* Parse raw url path and make it safe
*
* @param {String} urlPath The url path
* @return {String} Safe entry path
*/
parsePath(urlPath) {
let wlist = new RegExp('[^a-z0-9/\-]','g');
@ -105,6 +169,47 @@ module.exports = {
return _.join(urlParts, '/');
},
/**
* Gets the parent information.
*
* @param {String} entryPath The entry path
* @return {Object|False} The parent information.
*/
getParentInfo(entryPath) {
let self = this;
if(_.includes(entryPath, '/')) {
let parentParts = _.split(entryPath, '/');
let parentPath = _.join(_.initial(parentParts),'/');
let parentFile = _.last(parentParts);
let fpath = path.join(self._repoPath, parentPath + '.md');
return fs.statAsync(fpath).then((st) => {
if(st.isFile()) {
return fs.readFileAsync(fpath, 'utf8').then((contents) => {
let pageMeta = mark.parseMeta(contents);
return {
path: parentPath,
title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
};
});
} else {
return Promise.reject(new Error('Parent entry is not a valid file.'));
}
});
} else {
return Promise.reject(new Error('Parent entry is root.'));
}
}
};

View File

@ -29,7 +29,7 @@ var mkdown = md({
return '<pre><code>' + str + '</code></pre>';
}
}
return '<pre class="hljs"><code>' + hljs.highlightAuto(str).value + '</code></pre>';
return '<pre><code>' + str + '</code></pre>';
}
})
.use(mdEmoji)
@ -175,6 +175,10 @@ module.exports = {
html: parseContent(content),
tree: parseTree(content)
};
}
},
parseContent,
parseMeta,
parseTree
};

View File

@ -34,6 +34,7 @@
"bcryptjs-then": "^1.0.1",
"bluebird": "^3.4.1",
"body-parser": "^1.15.2",
"bson": "^0.5.4",
"bulma": "^0.1.2",
"cheerio": "^0.22.0",
"child-process-promise": "^2.1.3",
@ -48,6 +49,7 @@
"express-session": "^1.14.0",
"express-validator": "^2.20.8",
"farmhash": "^1.2.0",
"fs-extra": "^0.30.0",
"git-wrapper2-promise": "^0.2.9",
"highlight.js": "^9.6.0",
"i18next": "^3.4.1",
@ -69,10 +71,10 @@
"markdown-it-toc-and-anchor": "^4.1.1",
"moment": "^2.14.1",
"moment-timezone": "^0.5.5",
"msgpack5": "^3.4.0",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"pug": "^2.0.0-beta5",
"search-index": "^0.8.15",
"serve-favicon": "^2.3.0",
"simplemde": "^1.11.2",
"slug": "^0.9.1",

View File

@ -1,30 +1,33 @@
nav.nav.has-shadow.stickyscroll
.nav-left
a.nav-item.is-brand(href='/')
img(src='/favicons/android-icon-96x96.png', alt='Wiki')
a.nav-item(href='/')
h1.title Wiki
block rootNavLeft
a.nav-item.is-brand(href='/')
img(src='/favicons/android-icon-96x96.png', alt='Wiki')
a.nav-item(href='/')
h1.title Wiki
.nav-center
p.nav-item
input.input(type='text', placeholder='Search...', style= { 'max-width': '300px', width: '33vw' })
block rootNavCenter
p.nav-item
input.input(type='text', placeholder='Search...', style= { 'max-width': '300px', width: '33vw' })
span.nav-toggle
span
span
span
.nav-right.nav-menu
a.nav-item(href='#')
| History
a.nav-item(href='#')
| Source
span.nav-item
a.button
span.icon
i.fa.fa-edit
span Edit
a.button.is-primary(href='#', onclick='$(".modal").addClass("is-active");')
span.icon
i.fa.fa-plus
span Create
block rootNavRight
a.nav-item(href='#')
| History
a.nav-item(href='#')
| Source
span.nav-item
a.button
span.icon
i.fa.fa-edit
span Edit
a.button.is-primary(href='#', onclick='$(".modal").addClass("is-active");')
span.icon
i.fa.fa-plus
span Create

View File

@ -3,14 +3,22 @@ html
head
meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(charset='UTF-8')
meta(name='viewport', content='width=device-width, initial-scale=1')
meta(name='theme-color', content='#009688')
meta(name='msapplication-TileColor', content='#009688')
meta(name='msapplication-TileImage', content='/favicons/ms-icon-144x144.png')
title= appconfig.title
// Favicon
each favsize in [57, 60, 72, 76, 114, 120, 144, 152, 180]
link(rel='apple-touch-icon', sizes=favsize + 'x' + favsize, href='/favicons/apple-icon-' + favsize + 'x' + favsize + '.png')
link(rel='icon', type='image/png', sizes='192x192', href='/favicons/android-icon-192x192.png')
each favsize in [32, 96, 16]
link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize href='/images/favicon-' + favsize + 'x' + favsize + '.png')
link(rel='icon', type='image/png', sizes=favsize + 'x' + favsize, href='/favicons/favicon-' + favsize + 'x' + favsize + '.png')
link(rel='manifest', href='/manifest.json')
// CSS
link(href='https://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700|Inconsolata', rel='stylesheet', type='text/css')
link(type='text/css', rel='stylesheet', href='/css/libs.css')
link(type='text/css', rel='stylesheet', href='/css/app.css')
body(class='server-error')

24
views/pages/edit.pug Normal file
View File

@ -0,0 +1,24 @@
extends ../layout
block rootNavCenter
h2.nav-item= pageData.meta.title
block rootNavRight
a.nav-item(href='#')
| History
a.nav-item(href='#')
| Source
span.nav-item
a.button.is-danger(href='/' + pageData.meta.path)
span.icon
i.fa.fa-times
span Discard
a.button.is-success(href='#', onclick='$(".modal").addClass("is-active");')
span.icon
i.fa.fa-check
span Save Changes
block content
section.section.is-small
textarea#mk-editor= pageData.markdown

View File

@ -8,6 +8,21 @@ mixin tocMenu(ti)
ul
+tocMenu(node.nodes)
block rootNavRight
a.nav-item(href='#')
| History
a.nav-item(href='#')
| Source
span.nav-item
a.button(href='/edit/' + pageData.meta.path)
span.icon
i.fa.fa-edit
span Edit
a.button.is-primary(href='#', onclick='$(".modal").addClass("is-active");')
span.icon
i.fa.fa-plus
span Create
block content
section.section
@ -23,8 +38,9 @@ block content
ul.menu-list
li
a(href='/') Home
li
a(href='/') Storage
if pageData.parent
li
a(href='/' + pageData.parent.path)= pageData.parent.title
li
a(href='/account') Account
.box.stickyscroll(data-margin-top=70)