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( $ ) { jQuery( document ).ready(function( $ ) {
// Scroll
$('a').smoothScroll({ $('a').smoothScroll({
speed: 400, speed: 400,
offset: -20 offset: -20
@ -9,6 +11,8 @@ jQuery( document ).ready(function( $ ) {
var sticky = new Sticky('.stickyscroll'); var sticky = new Sticky('.stickyscroll');
// Alerts
var alerts = new Alerts(); var alerts = new Alerts();
if(alertsData) { if(alertsData) {
_.forEach(alertsData, (alertRow) => { _.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/_fonts';
@import './layout/_base'; @import './layout/_base';
$warning: #f68b39; $red: #E53935;
$orange: #FB8C00;
$blue: #039BE5;
$turquoise: #00ACC1;
$green: #7CB342;
$warning: $orange;
@import 'bulma'; @import 'bulma';
@import './libs/twemoji-awesome'; @import './libs/twemoji-awesome';
@import './libs/animate.min.css'; @import './libs/animate.min.css';
@import './components/_alerts'; @import './components/_alerts';
@import './components/_editor';
@import './layout/_header'; @import './layout/_header';
@import './layout/_footer'; @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 { .mkcontent {
h1 { 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; padding: 0;
border-radius: 3px;
> code { > code {
box-shadow: inset 0 0 5px 0 $grey-light; box-shadow: inset 0 0 5px 0 $grey-light;
border-radius: 5px;
} }
} }
@ -54,6 +77,10 @@
color: $grey-dark; color: $grey-dark;
} }
.twa {
font-size: 120%;
}
} }
.content a:not(.button):visited { .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 express = require('express');
var router = express.Router(); var router = express.Router();
var _ = require('lodash');
router.get('/edit/*', (req, res, next) => { 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) => { router.get('/new/*', (req, res, next) => {
@ -19,13 +42,13 @@ router.get('/*', (req, res, next) => {
let safePath = entries.parsePath(req.path); let safePath = entries.parsePath(req.path);
entries.fetch(safePath).then((pageData) => { entries.fetch(safePath).then((pageData) => {
console.log(pageData);
if(pageData) { if(pageData) {
res.render('pages/view', { pageData }); return res.render('pages/view', { pageData });
} else { } else {
next(); return next();
} }
}).catch((err) => { }).catch((err) => {
winston.error(err);
next(); next();
}); });

View File

@ -23,7 +23,8 @@ var paths = {
'./node_modules/jquery/dist/jquery.min.js', './node_modules/jquery/dist/jquery.min.js',
'./node_modules/vue/dist/vue.min.js', './node_modules/vue/dist/vue.min.js',
'./node_modules/jquery-smooth-scroll/jquery.smooth-scroll.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: [ scriptapps: [
'./client/js/components/*.js', './client/js/components/*.js',
@ -34,7 +35,8 @@ var paths = {
], ],
csslibs: [ csslibs: [
'./node_modules/font-awesome/css/font-awesome.min.css', './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: [ cssapps: [
'./client/scss/app.scss' './client/scss/app.scss'

View File

@ -5,7 +5,8 @@ var Promise = require('bluebird'),
fs = Promise.promisifyAll(require("fs")), fs = Promise.promisifyAll(require("fs")),
_ = require('lodash'), _ = require('lodash'),
farmhash = require('farmhash'), farmhash = require('farmhash'),
msgpack = require('msgpack5')(); BSONModule = require('bson'),
BSON = new BSONModule.BSONPure.BSON();
/** /**
* Entries Model * 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) { fetch(entryPath) {
let self = this; let self = this;
let fpath = path.join(self._repoPath, entryPath + '.md'); let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
let cpath = path.join(self._cachePath, farmhash.fingerprint32(entryPath) + '.bin');
return fs.statAsync(cpath).then((st) => { return fs.statAsync(cpath).then((st) => {
return st.isFile(); return st.isFile();
@ -47,10 +53,10 @@ module.exports = {
if(isCache) { if(isCache) {
console.log('from cache!'); // Load from cache
return fs.readFileAsync(cpath, 'utf8').then((contents) => { return fs.readFileAsync(cpath).then((contents) => {
return msgpack.decode(contents); return BSON.deserialize(contents);
}).catch((err) => { }).catch((err) => {
winston.error('Corrupted cache file. Deleting it...'); winston.error('Corrupted cache file. Deleting it...');
fs.unlinkSync(cpath); fs.unlinkSync(cpath);
@ -59,38 +65,96 @@ module.exports = {
} else { } else {
console.log('original!'); // Load original
// Parse original and cache it return self.fetchOriginal(entryPath);
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;
}
});
} }
}); });
},
/**
* 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) { parsePath(urlPath) {
let wlist = new RegExp('[^a-z0-9/\-]','g'); let wlist = new RegExp('[^a-z0-9/\-]','g');
@ -105,6 +169,47 @@ module.exports = {
return _.join(urlParts, '/'); 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><code>' + str + '</code></pre>';
} }
} }
return '<pre class="hljs"><code>' + hljs.highlightAuto(str).value + '</code></pre>'; return '<pre><code>' + str + '</code></pre>';
} }
}) })
.use(mdEmoji) .use(mdEmoji)
@ -175,6 +175,10 @@ module.exports = {
html: parseContent(content), html: parseContent(content),
tree: parseTree(content) tree: parseTree(content)
}; };
} },
parseContent,
parseMeta,
parseTree
}; };

View File

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

View File

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

View File

@ -3,14 +3,22 @@ html
head head
meta(http-equiv='X-UA-Compatible', content='IE=edge') meta(http-equiv='X-UA-Compatible', content='IE=edge')
meta(charset='UTF-8') 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 title= appconfig.title
// Favicon // 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] 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 // 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') link(type='text/css', rel='stylesheet', href='/css/app.css')
body(class='server-error') 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 ul
+tocMenu(node.nodes) +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 block content
section.section section.section
@ -23,8 +38,9 @@ block content
ul.menu-list ul.menu-list
li li
a(href='/') Home a(href='/') Home
li if pageData.parent
a(href='/') Storage li
a(href='/' + pageData.parent.path)= pageData.parent.title
li li
a(href='/account') Account a(href='/account') Account
.box.stickyscroll(data-margin-top=70) .box.stickyscroll(data-margin-top=70)