295 lines
8.3 KiB
JavaScript
295 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
const marked = require('marked');
|
|
|
|
function decamelize(str, sep) {
|
|
sep = sep === undefined ? '-' : sep;
|
|
return str
|
|
.replace(/([a-z\d])([A-Z])/g, '$1' + sep + '$2')
|
|
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1' + sep + '$2')
|
|
.toLowerCase();
|
|
}
|
|
|
|
function* range(start=0, end=null, step=1) {
|
|
if (end == null) {
|
|
end = start;
|
|
start = 0;
|
|
}
|
|
|
|
for (let i = start; i < end; i += step) {
|
|
yield i;
|
|
}
|
|
}
|
|
|
|
function _formatUnits(number, base, suffixes, callback) {
|
|
if (!number && number !== 0) {
|
|
return NaN;
|
|
}
|
|
number *= 1.0;
|
|
let suffix = suffixes.shift();
|
|
while (number >= base && suffixes.length > 0) {
|
|
suffix = suffixes.shift();
|
|
number /= base;
|
|
}
|
|
if (callback === undefined) {
|
|
callback = (number, suffix) => {
|
|
return suffix ? number.toFixed(1) + suffix : number;
|
|
};
|
|
}
|
|
return callback(number, suffix);
|
|
}
|
|
|
|
function formatFileSize(fileSize) {
|
|
return _formatUnits(
|
|
fileSize,
|
|
1024,
|
|
['B', 'K', 'M', 'G'],
|
|
(number, suffix) => {
|
|
const decimalPlaces = number < 20 && suffix !== 'B' ? 1 : 0;
|
|
return number.toFixed(decimalPlaces) + suffix;
|
|
});
|
|
}
|
|
|
|
function formatRelativeTime(timeString) {
|
|
if (!timeString) {
|
|
return 'never';
|
|
}
|
|
|
|
const then = Date.parse(timeString);
|
|
const now = Date.now();
|
|
const difference = Math.abs(now - then) / 1000.0;
|
|
const future = now < then;
|
|
|
|
const descriptions = [
|
|
[60, 'a few seconds', null],
|
|
[60 * 2, 'a minute', null],
|
|
[60 * 60, '% minutes', 60],
|
|
[60 * 60 * 2, 'an hour', null],
|
|
[60 * 60 * 24, '% hours', 60 * 60],
|
|
[60 * 60 * 24 * 2, 'a day', null],
|
|
[60 * 60 * 24 * 30.42, '% days', 60 * 60 * 24],
|
|
[60 * 60 * 24 * 30.42 * 2, 'a month', null],
|
|
[60 * 60 * 24 * 30.42 * 12, '% months', 60 * 60 * 24 * 30.42],
|
|
[60 * 60 * 24 * 30.42 * 12 * 2, 'a year', null],
|
|
[8640000000000000 /*max*/, '% years', 60 * 60 * 24 * 30.42 * 12],
|
|
];
|
|
|
|
let text = null;
|
|
for (let kv of descriptions) {
|
|
const multiplier = kv[0];
|
|
const template = kv[1];
|
|
const divider = kv[2];
|
|
if (difference < multiplier) {
|
|
text = template.replace(/%/, Math.round(difference / divider));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (text === 'a day') {
|
|
return future ? 'tomorrow' : 'yesterday';
|
|
}
|
|
return future ? 'in ' + text : text + ' ago';
|
|
}
|
|
|
|
function formatMarkdown(text) {
|
|
const renderer = new marked.Renderer();
|
|
|
|
const options = {
|
|
renderer: renderer,
|
|
breaks: true,
|
|
sanitize: true,
|
|
smartypants: true,
|
|
};
|
|
|
|
const sjis = [];
|
|
|
|
const preDecorator = text => {
|
|
text = text.replace(
|
|
/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig,
|
|
(match, capture) => {
|
|
var ret = '%%%SJIS' + sjis.length;
|
|
sjis.push(capture);
|
|
return ret;
|
|
});
|
|
//prevent ^#... from being treated as headers, due to tag permalinks
|
|
text = text.replace(/^#/g, '%%%#');
|
|
//fix \ before ~ being stripped away
|
|
text = text.replace(/\\~/g, '%%%T');
|
|
//post, user and tags premalinks
|
|
text = text.replace(
|
|
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
|
'$1[$2]($2)');
|
|
text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)');
|
|
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)');
|
|
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)');
|
|
return text;
|
|
};
|
|
|
|
const postDecorator = text => {
|
|
//restore fixes
|
|
text = text.replace(/%%%T/g, '\\~');
|
|
text = text.replace(/%%%#/g, '#');
|
|
|
|
text = text.replace(
|
|
/(?:<p>)?%%%SJIS(\d+)(?:<\/p>)?/,
|
|
(match, capture) => {
|
|
return '<div class="sjis">' + sjis[capture] + '</div>';
|
|
});
|
|
|
|
//search permalinks
|
|
text = text.replace(
|
|
/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig,
|
|
'<a href="/posts/query=$1"><code>$1</code></a>');
|
|
//spoilers
|
|
text = text.replace(
|
|
/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig,
|
|
'<span class="spoiler">$1</span>');
|
|
//[small]
|
|
text = text.replace(
|
|
/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig,
|
|
'<small>$1</small>');
|
|
//strike-through
|
|
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
|
|
text = text.replace(/\\~/g, '~');
|
|
return text;
|
|
};
|
|
|
|
return postDecorator(marked(preDecorator(text), options));
|
|
}
|
|
|
|
function formatUrlParameters(dict) {
|
|
let result = [];
|
|
for (let key of Object.keys(dict)) {
|
|
const value = dict[key];
|
|
if (key === 'parameters') {
|
|
continue;
|
|
}
|
|
if (value) {
|
|
result.push(`${key}=${value}`);
|
|
}
|
|
}
|
|
return result.join(';');
|
|
}
|
|
|
|
function splitByWhitespace(str) {
|
|
return str.split(/\s+/).filter(s => s);
|
|
}
|
|
|
|
function parseUrlParameters(query) {
|
|
let result = {};
|
|
for (let word of (query || '').split(/;/)) {
|
|
const [key, value] = word.split(/=/, 2);
|
|
result[key] = value;
|
|
}
|
|
result.query = result.query || '';
|
|
result.page = parseInt(result.page || '1');
|
|
return result;
|
|
}
|
|
|
|
function parseUrlParametersRoute(ctx, next) {
|
|
// ctx.parameters = {"user":...,"action":...} from /users/:user/:action
|
|
// ctx.parameters.parameters = value of :parameters as per /url/:parameters
|
|
Object.assign(
|
|
ctx.parameters,
|
|
parseUrlParameters(ctx.parameters.parameters));
|
|
next();
|
|
}
|
|
|
|
function unindent(callSite, ...args) {
|
|
function format(str) {
|
|
let size = -1;
|
|
return str.replace(/\n(\s+)/g, (m, m1) => {
|
|
if (size < 0) {
|
|
size = m1.replace(/\t/g, ' ').length;
|
|
}
|
|
return '\n' + m1.slice(Math.min(m1.length, size));
|
|
});
|
|
}
|
|
if (typeof callSite === 'string') {
|
|
return format(callSite);
|
|
}
|
|
if (typeof callSite === 'function') {
|
|
return (...args) => format(callSite(...args));
|
|
}
|
|
let output = callSite
|
|
.slice(0, args.length + 1)
|
|
.map((text, i) => (i === 0 ? '' : args[i - 1]) + text)
|
|
.join('');
|
|
return format(output);
|
|
}
|
|
|
|
function enableExitConfirmation() {
|
|
window.onbeforeunload = e => {
|
|
return 'Are you sure you want to leave? ' +
|
|
'Data you have entered may not be saved.';
|
|
};
|
|
}
|
|
|
|
function disableExitConfirmation() {
|
|
window.onbeforeunload = null;
|
|
}
|
|
|
|
function confirmPageExit() {
|
|
if (!window.onbeforeunload) {
|
|
return true;
|
|
}
|
|
if (window.confirm(window.onbeforeunload())) {
|
|
disableExitConfirmation();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function makeCssName(text, suffix) {
|
|
return suffix + '-' + text.replace(/[^a-z0-9]/g, '_');
|
|
}
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe.toString()
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function arraysDiffer(source1, source2) {
|
|
source1 = [...source1];
|
|
source2 = [...source2];
|
|
return (
|
|
source1.filter(value => !source2.includes(value)).length > 0 ||
|
|
source2.filter(value => !source1.includes(value)).length > 0);
|
|
}
|
|
|
|
function arrayToObject(array, keySelector, valueSelector) {
|
|
if (keySelector === undefined) {
|
|
keySelector = item => item;
|
|
}
|
|
if (valueSelector === undefined) {
|
|
valueSelector = item => item;
|
|
}
|
|
return array.reduce((obj, item) => {
|
|
obj[keySelector(item)] = valueSelector(item);
|
|
return obj;
|
|
}, {});
|
|
}
|
|
|
|
module.exports = arrayToObject([
|
|
range,
|
|
formatUrlParameters,
|
|
parseUrlParameters,
|
|
parseUrlParametersRoute,
|
|
formatRelativeTime,
|
|
formatFileSize,
|
|
formatMarkdown,
|
|
unindent,
|
|
enableExitConfirmation,
|
|
disableExitConfirmation,
|
|
confirmPageExit,
|
|
escapeHtml,
|
|
makeCssName,
|
|
splitByWhitespace,
|
|
arraysDiffer,
|
|
decamelize,
|
|
arrayToObject,
|
|
], func => func.name);
|