Browse Source

preparing for 3.0

master
Christian Müller 8 years ago
parent
commit
ab8a2d874c
  1. 3
      Makefile
  2. 26
      README.md
  3. 234
      api_spec.js
  4. 2
      assets/footer.html
  5. 36
      assets/markdown/Demo.md
  6. 0
      assets/markdown/TOS.md
  7. 0
      assets/public/favicon.ico
  8. 5
      assets/public/index.html
  9. 0
      assets/public/robots.txt
  10. 0
      assets/public/style.css
  11. 0
      assets/template.html
  12. 39
      package.json
  13. 45
      resources/edit.html
  14. 73
      resources/public/js/publishing.js
  15. 181
      server.js
  16. 64
      storage.js
  17. 55
      view.js

3
Makefile

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
push:
git push prod
git push origin

26
README.md

@ -1,28 +1,12 @@ @@ -1,28 +1,12 @@
# README
## About
[NoteHub](https://www.notehub.org) is a free and hassle-free pastebin for one-off markdown publishing and was implemented as a one-app-one-language [experiment](https://notehub.org/ikqz8).
## Implementation
NoteHub's implementation aims for ultimate simplicity and performance.
> "Make every detail perfect and limit the number of details to perfect." 
> — _Jack Dorsey_
### Design
## About
1. At a request, the server first checks, if a note is already rendered and is present in the LRU cache.
(a) If _yes_, it return the rendered HTML code and increases the views counter.
(b) Otherwise, the note is retrieved from the DB, rendered and put into the LRU cache; the views counter will be increased.
2. The rendering of note pages: there are HTML file tempates with placeholders, which will be trivially filled with replacements.
3. The LRU cache holds the rendered HTML for the most popular notes, which makes their access a static O(1) operation without any DB I/O.
4. The server keeps corresponding models for all notes in the cache for statistics updates. These models are persisted every 5 minutes to the DB.
Dead simple hosting for markdown notes.
## Installation & Deployment
To run NoteHub as a standalone version, execute:
1. `git clone https://github.com/chmllr/NoteHub.git`
2. `npm install`
3. `npm start`
This starts a NoteHub instance on port 3000.
TBD

234
api_spec.js

@ -1,234 +0,0 @@ @@ -1,234 +0,0 @@
var frisby = require('frisby');
var md5 = require('md5');
frisby.create('Landing page')
.get('http://localhost:3000/')
.expectStatus(200)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Hassle-free')
.toss();
frisby.create('Open note page')
.get('http://localhost:3000/new')
.expectStatus(200)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Terms of Service')
.toss();
frisby.create('Open TOS')
.get('http://localhost:3000/TOS')
.expectStatus(200)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Site Terms of Use Modifications')
.toss();
frisby.create('Incurrect URL')
.get('http://localhost:3000/abcdef')
.expectStatus(404)
.expectBodyContains('Not found')
.toss();
frisby.create('Invalid posting')
.post('http://localhost:3000/note')
.expectStatus(400)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Bad request')
.toss();
let testNote = 'This is a test note';
frisby.create('Invalid posting 2')
.post('http://localhost:3000/note', {
action: 'POST',
note: testNote
})
.expectStatus(400)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Bad request')
.toss();
frisby.create('Invalid posting 3')
.post('http://localhost:3000/note', {
action: 'POST',
session: md5('new'),
signature: 'assdss',
note: testNote
})
.expectStatus(400)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Signature mismatch')
.toss();
frisby.create('Valid posting')
.post('http://localhost:3000/note', {
action: 'POST',
session: md5('new'),
signature: md5(md5('new') + testNote),
password: '',
note: testNote
})
.expectStatus(302)
.expectBodyContains('Found. Redirecting to')
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
.after(function(err, res, body) {
let noteId = body.replace('Found. Redirecting to /', '');
frisby.create('Read posted note')
.get('http://localhost:3000/' + noteId)
.expectStatus(200)
.expectBodyContains(testNote)
.after((err, res, body) => {
frisby.create('Illegal note editing attempt with empty password')
.post('http://localhost:3000/note', {
id: noteId,
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + testNote+'!!!'),
note: testNote + '!!!',
password: ''
})
.expectStatus(400)
.expectBodyContains('Password is wrong')
.toss();
})
.after((err, res, body) => {
frisby.create('Illegal note editing attempt')
.post('http://localhost:3000/note', {
id: noteId,
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + testNote+'!!!'),
note: testNote + '!!!',
password: 'aaabbb'
})
.expectStatus(400)
.expectBodyContains('Password is wrong')
.toss();
})
.toss();
})
.toss();
frisby.create('Valid posting, editing and removal')
.post('http://localhost:3000/note', {
action: 'POST',
session: md5('new'),
signature: md5(md5('new') + testNote),
password: 'aabbcc',
note: testNote
})
.expectStatus(302)
.expectBodyContains('Found. Redirecting to')
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
.after(function(err, res, body) {
var noteId = body.replace('Found. Redirecting to /', '');
frisby.create('Export posted note')
.get('http://localhost:3000/' + noteId + '/export')
.expectStatus(200)
.expectHeaderContains('content-type', 'text/plain; charset=utf-8')
.expectBodyContains(testNote)
.toss();
frisby.create('Read posted note')
.get('http://localhost:3000/' + noteId)
.expectStatus(200)
.expectBodyContains(testNote)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.after((err, res, body) => {
frisby.create('Unauthorized note editing attempt')
.post('http://localhost:3000/note', {
id: noteId,
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + testNote+'!!!'),
note: testNote + '!!!',
password: 'abbcc'
})
.expectStatus(400)
.expectBodyContains('Password is wrong')
.toss();
})
.after((err, res, body) => {
frisby.create('Valid note editing attempt')
.post('http://localhost:3000/note', {
id: noteId,
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + 'Changed!'),
note: 'Changed!',
password: 'aabbcc'
})
.expectStatus(302)
.after((err, res, body) => {
frisby.create('Read changed note')
.get('http://localhost:3000/' + noteId)
.expectStatus(200)
.expectBodyContains('Changed!')
.toss();
})
.after((err, res, body) => {
frisby.create('Delete posted note')
.post('http://localhost:3000/note',{
id: noteId,
button: 'Delete',
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + 'Changed!'),
note: 'Changed!',
password: 'aabbcc'
})
.expectStatus(200)
.expectBodyContains('Note deleted')
.toss();
})
.toss();
})
.toss();
frisby.create('Read stats of posted note')
.expectStatus(200)
.get('http://localhost:3000/' + noteId).toss();
frisby.create('Read stats of posted note')
.expectStatus(200)
.get('http://localhost:3000/' + noteId).toss();
frisby.create('Read stats of posted note')
.expectStatus(200)
.get('http://localhost:3000/' + noteId).toss();
frisby.create('Read stats of posted note')
.get('http://localhost:3000/' + noteId + '/stats')
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectStatus(200)
.expectBodyContains('Statistics')
.expectBodyContains('<tr><td>Views</td><td>4</td></tr>')
.toss();
})
.toss();
var tooLongNote = 'ABCD';
while (tooLongNote.length < 1024*200) tooLongNote += tooLongNote;
frisby.create('Invalid posting of too long note')
.post('http://localhost:3000/note', {
action: 'POST',
session: md5('new'),
signature: md5(md5('new') + testNote),
password: 'aabbcc',
note: tooLongNote
})
.expectStatus(400)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('too large')
.toss();
frisby.create('Invalid update without id')
.post('http://localhost:3000/note', {
action: 'UPDATE',
session: md5('new'),
signature: md5(md5('new') + 'Any note'),
password: 'aabbcc',
note: 'Any note'
})
.expectStatus(400)
.expectHeaderContains('content-type', 'text/html; charset=utf-8')
.expectBodyContains('Wrong note ID')
.toss();

2
resources/footer.html → assets/footer.html

@ -3,5 +3,5 @@ @@ -3,5 +3,5 @@
<a href="%LINK%/stats">statistics</a> &middot;
<a href="%LINK%/edit">edit</a> &middot;
<a href="%LINK%/export">export</a> &middot;
<a href="/TOS">terms of service</a>
<a href="/TOS.md">terms of service</a>
</footer>

36
assets/markdown/Demo.md

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
# Demo Note
## Text Formatting
This is a _short_ note demonstrating the **capabilities** of Markdown. [Markdown](http://en.wikipedia.org/wiki/Markdown) is a _markup language_ with plain text
formatting syntax. But you also can use <u>standard HTML</u> tags.
## Backquotes, Lists & Code
This is a backquote:
> _"Our greatest glory is not in never falling but in rising every time we fall."_
> — Confucius
To create simple lists, just enumerate all items using a dash in the prefix:
- Alpha
- Beta
- Gamma
Also you can either mark some special `words` or write entire `code` blocks:
(defn fact [n]
(if (< n 2) 1
(* n (fact (- n 1)))))
## Tables
Also simple tables is a piece of cake:
Column 1 | Column 2 | Column 3
--- | --- | ---
Text 1 | Text 3 | <s>Text 5</s>
Text 2 | Text 4 | <mark>Text 6</mark>
Take a look at the [source code](/Demo.md/export) of this page, to see how it works.

0
resources/TOS.md → assets/markdown/TOS.md

0
resources/public/favicon.ico → assets/public/favicon.ico

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

5
resources/public/index.html → assets/public/index.html

@ -11,13 +11,14 @@ @@ -11,13 +11,14 @@
<h1>NoteHub</h1>
<h2>Free and Hassle-free Pastebin for Markdown Notes</h2>
<br>
<a class="landing-button demo" href="/demo" style="color: white">See Demo Note</a>
<a class="landing-button demo" href="/Demo.md" style="color: white">See Demo Note</a>
<a class="landing-button" href="/new" style="color: white">New Note</a>
</div>
<div id="dashed-line"></div>
<article class="bottom-space">
<h2>Changelog</h2>
<ul>
<li><strong>2017-08</strong>: NoteHub 3.0 released.</li>
<li><strong>2016-08</strong>: Syntax highlighting for code blocks added (thanks to <a href="https://github.com/maciejsmolinski">Maciej</a>).</li>
<li><strong>2016-03</strong>: Note deletion feature added.</li>
<li><strong>2015-10</strong>: NoteHub rewritten in Node.js.</li>
@ -34,7 +35,7 @@ @@ -34,7 +35,7 @@
</article>
<footer>
<a href="https://github.com/chmllr/NoteHub">source code</a> &middot;
<a href="/TOS">terms of service</a>
<a href="/TOS.md">terms of service</a>
</footer>
</body>
</html>

0
resources/public/robots.txt → assets/public/robots.txt

0
resources/public/style.css → assets/public/style.css

0
resources/template.html → assets/template.html

39
package.json

@ -1,39 +0,0 @@ @@ -1,39 +0,0 @@
{
"name": "NoteHub",
"version": "3.0.0",
"description": "Free Pastebin for One-Off Markdown Publishing",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "./node_modules/nodemon/bin/nodemon.js server.js",
"test": "jasmine-node ."
},
"repository": {
"type": "git",
"url": "https://github.com/chmllr/NoteHub.git"
},
"keywords": [
"markdown",
"pastebin"
],
"author": "Christian Müller <notehub@icloud.com> (http://twitter.com/drmllr)",
"license": "ISC",
"bugs": {
"url": "https://github.com/chmllr/NoteHub/issues"
},
"homepage": "https://github.com/chmllr/NoteHub",
"dependencies": {
"body-parser": "^1.15.0",
"express": "^4.13.4",
"highlight.js": "^9.5.0",
"lru-cache": "^4.0.0",
"marked": "^0.3.5",
"md5": "^2.1.0",
"sequelize": "^3.19.3",
"sqlite3": "*"
},
"devDependencies": {
"frisby": "^0.8.5",
"nodemon": "^1.9.1"
}
}

45
resources/edit.html

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
<!DOCTYPE html>
<html style="height: 100%;">
<head>
<title>NoteHub &mdash; New Page</title>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<link href="/style.css" rel="stylesheet" type="text/css" />
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.5/marked.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.5.0/highlight.min.js" type="text/javascript"></script>
<script src="/js/publishing.js" type="text/javascript"></script>
<base target="_blank">
</head>
<body onload="onLoad()" style="height: 100%">
<div id="editContainer">
<form id="editPane" action="/note" autocomplete="off" method="POST" target="_self">
<input id="action" name="action" value="%ACTION%" type="hidden" />
<input id="id" name="id" value="%ID%" type="hidden" />
<input id="password" name="password" type="hidden" />
<input id="session" name="session" type="hidden" value="%SESSION%" />
<input id="signature" name="signature" type="hidden" />
<textarea id="note" name="note">%CONTENT%</textarea>
<fieldset id="input-elems">
<input class="ui-elem" id="plain-password" name="plain-password"
placeholder="Password for editing" type="text" autocomplete="off">&nbsp;
<label style="margin-right: 1em">
<input id="tos" type="checkbox" onClick="enableButton()">
Accept <a href="/TOS">Terms of Service</a>
</label>
<input class="button ui-elem" disabled id="publish-button" name="button" type="submit" value="Publish">
<input class="button ui-elem" id="delete-button" name="button" type="submit" value="Delete">
<span id="tableau">
0 words
</span>
<br>
<br>
</fieldset>
</form>
<div id="previewPane"><article id="draft"></article></div>
</div>
</body>
</html>

73
resources/public/js/publishing.js

@ -1,73 +0,0 @@ @@ -1,73 +0,0 @@
var $ = function(id) {
return document.getElementById(id);
};
var iosDetected = navigator.userAgent.match('(iPad|iPod|iPhone)');
var timer = null;
var timerDelay = iosDetected ? 800 : 400;
var $note, $action, $preview, $plain_password, $tableau;
var backendTimer;
document.addEventListener('DOMContentLoaded', function () {
marked.setOptions({
langPrefix: 'hljs lang-',
highlight: function (code) {
return hljs.highlightAuto(code).value;
},
});
});
function md2html(input) {
return marked(input);
}
function saveDraft() {
if ($action == 'UPDATE') return;
console.log('draft autosave...');
$tableau.innerHTML = 'Draft autosaved.';
localStorage.setItem('draft', $note.value);
}
function enableButton() {
var checkbox = $('tos');
var button = $('publish-button');
button.disabled = !checkbox.checked;
}
function onLoad() {
$note = $('note');
$action = $('action').value;
$preview = $('draft');
$tableau = $('tableau');
$plain_password = $('plain-password');
var updatePreview = function() {
clearTimeout(timer);
var content = $note.value;
var delay = Math.min(timerDelay, timerDelay * (content.length / 400));
timer = setTimeout(function() {
$preview.innerHTML = md2html(content);
$tableau.innerHTML = content.split(/\s+/).length + ' words';
}, delay);
};
if ($action == 'UPDATE') updatePreview();
else {
$('delete-button').style.display = 'none';
$note.value = '';
var draft = localStorage.getItem('draft');
if (draft) {
$note.value = draft;
updatePreview();
}
}
$note.onkeyup = updatePreview;
$('delete-button').onclick = $('publish-button').onclick = function(e) {
localStorage.removeItem('draft');
self.onbeforeunload = null;
if ($plain_password.value !== '') $('password').value = md5($plain_password.value);
$plain_password.value = null;
$('signature').value = md5($('session').value + $note.value.replace(/[\n\r]/g, ''));
};
if (iosDetected) $note.className += ' ui-border';
else $note.focus();
self.onbeforeunload = saveDraft;
setInterval(saveDraft, 60 * 1000);
}

181
server.js

@ -1,181 +0,0 @@ @@ -1,181 +0,0 @@
var express = require('express');
var view = require('./view');
var storage = require('./storage');
var md5 = require('md5');
var LRU = require('lru-cache');
var bodyParser = require('body-parser');
var fs = require('fs');
var blackList = new Set();
var app = express();
app.use(bodyParser.urlencoded({ extended: true, limit: '200kb' }));
app.use(express.static(__dirname + '/resources/public'));
app.use(function (error, req, res, next) {
if (error) {
sendResponse(res, 400, 'Bad request', error.message);
log('REQUEST ERROR:', error);
} else {
next();
}
});
var MODELS = {};
var CACHE = new LRU({
max: 50,
dispose: key => {
log('disposing', key, 'from cache');
var model = MODELS[key];
if (model) model.save();
delete MODELS[key];
}
});
var log = function() {
var date = new Date();
var timestamp = date.getDate() + '/' + date.getMonth() + ' ' + date.getHours() + ':' +
date.getMinutes() + ':' + date.getSeconds() + '.' + date.getMilliseconds();
var message = Array.prototype.slice.call(arguments);
message.unshift('--');
message.unshift(timestamp);
console.log.apply(console, message);
};
app.get('/TOS', (req, res) => res.send(view.renderTOS()));
app.get('/new', (req, res) => {
log(req.ip, 'opens /new');
res.send(view.newNotePage(md5('new')));
});
app.post('/note', (req, res) => {
var body = req.body,
session = body.session,
note = body.note,
password = body.password,
action = body.action,
id = body.id;
log(req.ip, 'calls /note to', action, id);
var goToNote = note => res.redirect('/' + note.id);
if (!note || !session || session.indexOf(md5('edit/' + id)) !== 0 && session.indexOf(md5('new')) !== 0)
return sendResponse(res, 400, 'Bad request');
if (body.signature != md5(session + note.replace(/[\n\r]/g, '')))
return sendResponse(res, 400, 'Signature mismatch');
if (action == 'POST')
storage.addNote(note, password).then(goToNote);
else {
if (!id) return sendResponse(res, 400, 'Wrong note ID');
CACHE.del(id);
if (body.button == 'Delete') {
log('deleting note', id);
storage.deleteNote(id, password).then(
() => sendResponse(res, 200, 'Note deleted'),
error => sendResponse(res, 400, 'Bad request', error.message));
} else {
log('updating note', id);
storage.updateNote(id, password, note).then(goToNote,
error => sendResponse(res, 400, 'Bad request', error.message));
}
}
});
app.get('/:year/:month/:day/:title', (req, res) => {
var P = req.params, url = P.year + '/' + P.month + '/' + P.day + '/' + P.title;
log(req.ip, 'resolves deprecated id', url);
if (CACHE.has(url)) {
log(url, 'is cached!');
var id = CACHE.get(url);
if (id) res.redirect('/' + id);
else notFound(res);
} else storage.getNoteId(url).then(note => {
log(url, 'is not cached, resolving...');
if (note) {
CACHE.set(url, note.id);
res.redirect('/' + note.id);
} else {
CACHE.set(url, null);
notFound(res);
}
});
});
app.get(/\/([a-z0-9]+)\/edit/, (req, res) => {
var id = req.params['0'];
log(req.ip, 'calls /edit on', id);
storage.getNote(id).then(note => res.send(note ?
view.editNotePage(md5('edit/' + id), note) :
notFound(res)));
});
app.get(/\/([a-z0-9]+)\/export/, (req, res) => {
var id = req.params['0'];
log(req.ip, 'calls /export on', id);
res.set({ 'Content-Type': 'text/plain', 'Charset': 'utf-8' });
storage.getNote(id).then(note => note ?
res.send(note.text) :
notFound(res));
});
app.get(/\/([a-z0-9]+)\/stats/, (req, res) => {
var id = req.params['0'];
log(req.ip, 'calls /stats on', id);
var promise = id in MODELS ?
new Promise(resolve => resolve(MODELS[id])) :
storage.getNote(id);
promise.then(note => note ?
res.send(view.renderStats(note)) :
notFound(res));
});
app.get(/\/([a-z0-9]+)/, (req, res) => {
var id = req.params['0'];
log(req.ip, 'open note', id, 'from', req.get('Referer'));
if (CACHE.has(id)) {
log(id, 'is cached!');
var obj = MODELS[id];
if (!obj) return notFound(res);
obj.views++;
res.send(CACHE.get(id));
} else storage.getNote(id).then(note => {
log(id, 'is not cached, resolving...');
if (!note) {
CACHE.set(id, null);
return notFound(res);
}
var content = view.renderNote(note, blackList);
CACHE.set(id, content);
MODELS[id] = note;
note.views++;
res.send(content);
});
});
var sendResponse = (res, code, message, details) => {
log('sending response', code, message);
res.status(code).send(view.renderPage(null, message,
`<h1>${message}</h1><br/>` +
`<center>${details || '¯\\_(ツ)_/¯'}</center>`, ''));
};
var notFound = res => sendResponse(res, 404, 'Not found');
var server = app.listen(process.env.PORT || 3000,
() => log('NoteHub server listening on port', server.address().port));
setInterval(() => {
var keys = Object.keys(MODELS);
log('saving stats for', keys.length, 'models...');
keys.forEach(id => MODELS[id].save());
}, 5 * 60 * 1000);
var updateBlackList = () => {
var ids = fs.readFileSync(process.env.BLACK_LIST || '/dev/null', 'utf-8').split(/\n+/).filter(Boolean);
ids.filter(id => !blackList.has(id)).forEach(id => CACHE.del(id));
blackList = new Set(ids);
log('black list updated, entries:', blackList.size);
};
setInterval(updateBlackList, 60 * 60 * 1000);
updateBlackList();

64
storage.js

@ -1,64 +0,0 @@ @@ -1,64 +0,0 @@
var Sequelize = require('sequelize');
var sequelize = new Sequelize('database', null, null, {
dialect: 'sqlite',
pool: {
max: 5,
min: 0,
idle: 10000
},
storage: 'database.sqlite'
});
var Note = sequelize.define('Note', {
id: { type: Sequelize.STRING(6), unique: true, primaryKey: true },
deprecatedId: Sequelize.TEXT,
text: Sequelize.TEXT,
published: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
edited: { type: Sequelize.DATE, allowNull: true, defaultValue: null },
password: Sequelize.STRING(16),
views: { type: Sequelize.INTEGER, defaultValue: 0 }
});
sequelize.sync();
module.exports.getNote = id => Note.findById(id);
module.exports.getNoteId = deprecatedId => Note.findOne({
where: { deprecatedId: deprecatedId }
});
var generateId = () => [1, 1, 1, 1, 1]
.map(() => {
var code = Math.floor(Math.random() * 36);
return String.fromCharCode(code + (code < 10 ? 48 : 87));
})
.join('');
var getFreeId = () => {
var id = generateId();
return Note.findById(id).then(result => result ? getFreeId() : id);
};
module.exports.addNote = (note, password) => getFreeId().then(id => Note.create({
id: id,
text: note,
password: password
}));
var passwordCheck = (note, password, callback) =>
(!note || note.password.length === 0 || note.password !== password) ?
new Promise((resolve, reject) => reject({ message: 'Password is wrong' })) :
callback();
module.exports.updateNote = (id, password, text) =>
Note.findById(id).then(note =>
passwordCheck(note, password, () => {
note.text = text;
note.edited = new Date();
return note.save();
}));
module.exports.deleteNote = (id, password) =>
Note.findById(id).then(note =>
passwordCheck(note, password, () => note.destroy()));

55
view.js

@ -1,55 +0,0 @@ @@ -1,55 +0,0 @@
var marked = require('marked');
var fs = require('fs');
var hljs = require('highlight.js');
var TOS = fs.readFileSync('resources/TOS.md', 'utf-8');
var pageTemplate = fs.readFileSync('resources/template.html', 'utf-8');
var footerTemplate = fs.readFileSync('resources/footer.html', 'utf-8');
var editTemplate = fs.readFileSync('resources/edit.html', 'utf-8');
var header = fs.readFileSync(process.env.HEADER || '/dev/null', 'utf-8');
var deriveTitle = text => text
.split(/[\n\r]/)[0].slice(0,25)
.replace(/[`~!@#\$%^&\*_|\+=\?;:'",.<>\{\}\\\/]/g, '');
var renderPage = (id, title, content, footer, blackList) => pageTemplate
.replace('%HEADER%', blackList && blackList.has(id) ? header : '')
.replace('%TITLE%', title)
.replace('%CONTENT%', content.replace(/<meta.*?>/gi, '').replace(/<script[\s\S.]*?\/script>/gi, ''))
.replace('%FOOTER%', footer || '');
marked.setOptions({
langPrefix: 'hljs lang-',
highlight: code => hljs.highlightAuto(code).value,
});
module.exports.renderPage = renderPage;
module.exports.renderStats = note => renderPage(note.id, deriveTitle(note.text),
`<h2>Statistics</h2>
<table>
<tr><td>Published</td><td>${note.published}</td></tr>
<tr><td>Edited</td><td>${note.edited || 'N/A'}</td></tr>
<tr><td>Views</td><td>${note.views}</td></tr>
</table>`);
module.exports.renderTOS = () => renderPage('tos', 'Terms of Service', marked(TOS));
module.exports.renderNote = (note, blackList) => renderPage(note.id,
deriveTitle(note.text),
marked(note.text),
footerTemplate.replace(/%LINK%/g, note.id),
blackList);
module.exports.newNotePage = session => editTemplate
.replace('%ACTION%', 'POST')
.replace('%SESSION%', session)
.replace('%CONTENT%', 'Loading...');
module.exports.editNotePage = (session, note) => editTemplate
.replace('%ACTION%', 'UPDATE')
.replace('%SESSION%', session)
.replace('%ID%', note.id)
.replace('%CONTENT%', escape$(note.text));
var escape$ = s => s.split('').map(chr => chr == '$' ? '$$' : chr).join('');
Loading…
Cancel
Save