瀏覽代碼

Caching + Edit Mode UI

NGPixel 8 年之前
父節點
當前提交
4be54310c4

File diff suppressed because it is too large
+ 0 - 0
assets/css/app.css


File diff suppressed because it is too large
+ 0 - 0
assets/css/libs.css


+ 1 - 1
assets/js/app.js

@@ -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 it is too large
+ 0 - 0
assets/js/libs.js


+ 20 - 0
client/js/app.js

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

+ 8 - 1
client/scss/app.scss

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

+ 8 - 0
client/scss/components/_editor.scss

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

+ 29 - 2
client/scss/layout/_content.scss

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

+ 5 - 0
client/scss/layout/_header.scss

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

+ 27 - 4
controllers/pages.js

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

+ 4 - 2
gulpfile.js

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

+ 135 - 30
models/entries.js

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

+ 6 - 2
models/markdown.js

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

+ 3 - 1
package.json

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

+ 22 - 19
views/common/header.pug

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

+ 10 - 2
views/error.pug

@@ -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 - 0
views/pages/edit.pug

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

+ 18 - 2
views/pages/view.pug

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

Some files were not shown because too many files changed in this diff