Caching + Edit Mode UI

pull/1/head
NGPixel 10 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