Caching + Edit Mode UI

pull/1/head
NGPixel 8 years ago
parent 1d2893765c
commit 4be54310c4

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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

@ -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
});
}
});

@ -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';

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

@ -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 {

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

@ -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();
});

@ -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'

@ -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!');
// 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;
}
});
// Load original
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.'));
}
}
};

@ -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
};

@ -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",

@ -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

@ -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')

@ -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

@ -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)

Loading…
Cancel
Save