Webpack5 (#10311)
* feat: webpack 5 part 1 * fix: gruntfile fixes * fix: fix taskbar warning add app.importScript copy public/src/modules to build folder * refactor: remove commented old code * feat: reenable admin * fix: acp settings pages, fix sortable on manage categories embedded require in html not allowed * fix: bundle serialize/deserizeli so plugins dont break * test: fixe util tests * test: fix require path * test: more test fixes * test: require correct utils module * test: require correct utils * test: log stack * test: fix db require blowing up tests * test: move and disable bundle test * refactor: add aliases * test: disable testing route * fix: move webpack modules necessary for build, into `dependencies` * test: fix one more test remove 500-embed.tpl * fix: restore use of assets/nodebb.min.js, at least for now * fix: remove unnecessary line break * fix: point to proper ACP bundle * test: maybe fix build test * test: composer * refactor: dont need dist * refactor: more cleanup use everything from build/public folder * get rid of conditional import in app.js * fix: ace * refactor: cropper alias * test: lint and test fixes * lint: fix * refactor: rename function to app.require * refactor: go back to using app.require * chore: use github branch * chore: use webpack branch * feat: webpack webinstaller * feat: add chunkFile name with contenthash * refactor: move hooks to top * refactor: get rid of template500Function * fix(deps): use webpack5 branch of 2factor plugin * chore: tagging v2.0.0-beta.0 pre-release version 💥 :shipit: 🎉 🚀 * refactor: disable cache on templates loadTemplate is called once by benchpress and the result is cache internally * refactor: add server side helpers.js * feat: deprecate /plugins shorthand route, closes #10343 * refactor: use build/public for webpack * test: fix filename * fix: more specific selector * lint: ignore * refactor: fix comments * test: add debug for random failing test * refactor: cleanup remove test page, remove dupe functions in utils.common * lint: use relative path for now * chore: bump prerelease version * feat: add translateKeys * fix: optional params * fix: get rid of extra timeago files * refactor: cleanup, require timeago locale earlier remove translator.prepareDOM, it is in header.tpl html tag * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * feat: allow app.require('bootbox'/'benchpressjs') * refactor: require server side utils * test: jquery ready * change istaller to use build/public * test: use document.addEventListener * refactor: closes #10301 * refactor: generateTopicClass * fix: column counts for other privileges * fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking] * fix: typo in hook name * refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags) * fix: crash if `delay` not passed in (as it cannot be destructured) * refactor: replace substr * feat: set --panel-offset style in html element based on stored value in localStorage * refactor: addDropupHandler() logic to be less naive - Take into account height of the menu - Don't apply dropUp logic if there's nothing in the dropdown - Remove 'hidden' class (added by default in Persona for post tools) when menu items are added closes #10423 * refactor: simplify utils.params [breaking] Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do. * feat: add support for returning full URLSearchParams for utils.params * fix: utils.params() fallback handling * fix: default empty obj for params() * fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end * fix: utils.params() not allowing relative paths to be passed in * refactor(DRY): new assertPasswordValidity utils method * fix: incorrect error message returned on insufficient privilege on flag edit * fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate - added failing tests and patched up middleware.assert.flags to fix * refactor: flag api v3 tests to create new post and flags on every round * fix: missing error:no-flag language key * refactor: flags.canView to check flag existence, simplify middleware.assert.flag * feat: flag deletion API endpoint, #10426 * feat: UI for flag deletion, closes #10426 * chore: update plugin versions * chore: up emoji * chore: update markdown * chore: up emoji-android * fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check Co-authored-by: Julian Lam <julian@nodebb.org>isekai-main
parent
d6843294fc
commit
d20b07cfea
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
require('./app');
|
||||
|
||||
// scripts-client.js contains javascript files
|
||||
// from plugins that add files to "scripts" block in plugin.json
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
require('../scripts-client');
|
@ -0,0 +1,20 @@
|
||||
export * from 'ace-builds';
|
||||
|
||||
// only import the modes and theme we use
|
||||
import 'ace-builds/src-noconflict/mode-javascript';
|
||||
import 'ace-builds/src-noconflict/mode-less';
|
||||
import 'ace-builds/src-noconflict/mode-html';
|
||||
import 'ace-builds/src-noconflict/ext-searchbox';
|
||||
import 'ace-builds/src-noconflict/theme-twilight';
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
import htmlWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-html';
|
||||
import javascriptWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-javascript';
|
||||
import cssWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-css';
|
||||
|
||||
ace.config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
|
||||
ace.config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
|
||||
ace.config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl);
|
||||
|
||||
|
@ -0,0 +1,347 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (utils, Benchpress, relative_path) {
|
||||
Benchpress.setGlobal('true', true);
|
||||
Benchpress.setGlobal('false', false);
|
||||
|
||||
const helpers = {
|
||||
displayMenuItem,
|
||||
buildMetaTag,
|
||||
buildLinkTag,
|
||||
stringify,
|
||||
escape,
|
||||
stripTags,
|
||||
generateCategoryBackground,
|
||||
generateChildrenCategories,
|
||||
generateTopicClass,
|
||||
membershipBtn,
|
||||
spawnPrivilegeStates,
|
||||
localeToHTML,
|
||||
renderTopicImage,
|
||||
renderTopicEvents,
|
||||
renderEvents,
|
||||
renderDigestAvatar,
|
||||
userAgentIcons,
|
||||
buildAvatar,
|
||||
register,
|
||||
__escape: identity,
|
||||
};
|
||||
|
||||
function identity(str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function displayMenuItem(data, index) {
|
||||
const item = data.navigation[index];
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildMetaTag(tag) {
|
||||
const name = tag.name ? 'name="' + tag.name + '" ' : '';
|
||||
const property = tag.property ? 'property="' + tag.property + '" ' : '';
|
||||
const content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : '';
|
||||
|
||||
return '<meta ' + name + property + content + '/>\n\t';
|
||||
}
|
||||
|
||||
function buildLinkTag(tag) {
|
||||
const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin'];
|
||||
const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attr => (tag[attr] ? `${attr}="${tag[attr]}" ` : ''));
|
||||
|
||||
return '<link ' + link + rel + as + type + sizes + title + href + crossorigin + '/>\n\t';
|
||||
}
|
||||
|
||||
function stringify(obj) {
|
||||
// Turns the incoming object into a JSON string
|
||||
return JSON.stringify(obj).replace(/&/gm, '&').replace(/</gm, '<').replace(/>/gm, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function escape(str) {
|
||||
return utils.escapeHTML(str);
|
||||
}
|
||||
|
||||
function stripTags(str) {
|
||||
return utils.stripHTMLTags(str);
|
||||
}
|
||||
|
||||
function generateCategoryBackground(category) {
|
||||
if (!category) {
|
||||
return '';
|
||||
}
|
||||
const style = [];
|
||||
|
||||
if (category.bgColor) {
|
||||
style.push('background-color: ' + category.bgColor);
|
||||
}
|
||||
|
||||
if (category.color) {
|
||||
style.push('color: ' + category.color);
|
||||
}
|
||||
|
||||
if (category.backgroundImage) {
|
||||
style.push('background-image: url(' + category.backgroundImage + ')');
|
||||
if (category.imageClass) {
|
||||
style.push('background-size: ' + category.imageClass);
|
||||
}
|
||||
}
|
||||
|
||||
return style.join('; ') + ';';
|
||||
}
|
||||
|
||||
function generateChildrenCategories(category) {
|
||||
let html = '';
|
||||
if (!category || !category.children || !category.children.length) {
|
||||
return html;
|
||||
}
|
||||
category.children.forEach(function (child) {
|
||||
if (child && !child.isSection) {
|
||||
const link = child.link ? child.link : (relative_path + '/category/' + child.slug);
|
||||
html += '<span class="category-children-item pull-left">' +
|
||||
'<div role="presentation" class="icon pull-left" style="' + generateCategoryBackground(child) + '">' +
|
||||
'<i class="fa fa-fw ' + child.icon + '"></i>' +
|
||||
'</div>' +
|
||||
'<a href="' + link + '"><small>' + child.name + '</small></a></span>';
|
||||
}
|
||||
});
|
||||
html = html ? ('<span class="category-children">' + html + '</span>') : html;
|
||||
return html;
|
||||
}
|
||||
|
||||
function generateTopicClass(topic) {
|
||||
const fields = ['locked', 'pinned', 'deleted', 'unread', 'scheduled'];
|
||||
return fields.filter(field => !!topic[field]).join(' ');
|
||||
}
|
||||
|
||||
// Groups helpers
|
||||
function membershipBtn(groupObj) {
|
||||
if (groupObj.isMember && groupObj.name !== 'administrators') {
|
||||
return '<button class="btn btn-danger" data-action="leave" data-group="' + groupObj.displayName + '"' + (groupObj.disableLeave ? ' disabled' : '') + '><i class="fa fa-times"></i> [[groups:membership.leave-group]]</button>';
|
||||
}
|
||||
|
||||
if (groupObj.isPending && groupObj.name !== 'administrators') {
|
||||
return '<button class="btn btn-warning disabled"><i class="fa fa-clock-o"></i> [[groups:membership.invitation-pending]]</button>';
|
||||
} else if (groupObj.isInvited) {
|
||||
return '<button class="btn btn-link" data-action="rejectInvite" data-group="' + groupObj.displayName + '">[[groups:membership.reject]]</button><button class="btn btn-success" data-action="acceptInvite" data-group="' + groupObj.name + '"><i class="fa fa-plus"></i> [[groups:membership.accept-invitation]]</button>';
|
||||
} else if (!groupObj.disableJoinRequests && groupObj.name !== 'administrators') {
|
||||
return '<button class="btn btn-success" data-action="join" data-group="' + groupObj.displayName + '"><i class="fa fa-plus"></i> [[groups:membership.join-group]]</button>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function spawnPrivilegeStates(member, privileges) {
|
||||
const states = [];
|
||||
for (const priv in privileges) {
|
||||
if (privileges.hasOwnProperty(priv)) {
|
||||
states.push({
|
||||
name: priv,
|
||||
state: privileges[priv],
|
||||
});
|
||||
}
|
||||
}
|
||||
return states.map(function (priv) {
|
||||
const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create'];
|
||||
const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups'];
|
||||
const globalModDisabled = ['groups:moderate'];
|
||||
const disabled =
|
||||
(member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) ||
|
||||
(member === 'spiders' && !spidersEnabled.includes(priv.name)) ||
|
||||
(member === 'Global Moderators' && globalModDisabled.includes(priv.name));
|
||||
|
||||
return '<td class="text-center" data-privilege="' + priv.name + '" data-value="' + priv.state + '"><input autocomplete="off" type="checkbox"' + (priv.state ? ' checked' : '') + (disabled ? ' disabled="disabled"' : '') + ' /></td>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function localeToHTML(locale, fallback) {
|
||||
locale = locale || fallback || 'en-GB';
|
||||
return locale.replace('_', '-');
|
||||
}
|
||||
|
||||
function renderTopicImage(topicObj) {
|
||||
if (topicObj.thumb) {
|
||||
return '<img src="' + topicObj.thumb + '" class="img-circle user-img" title="' + topicObj.user.username + '" />';
|
||||
}
|
||||
return '<img component="user/picture" data-uid="' + topicObj.user.uid + '" src="' + topicObj.user.picture + '" class="user-img" title="' + topicObj.user.username + '" />';
|
||||
}
|
||||
|
||||
function renderTopicEvents(index, sort) {
|
||||
if (sort === 'most_votes') {
|
||||
return '';
|
||||
}
|
||||
const start = this.posts[index].eventStart;
|
||||
const end = this.posts[index].eventEnd;
|
||||
const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end);
|
||||
if (!events.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return renderEvents.call(this, events);
|
||||
}
|
||||
|
||||
function renderEvents(events) {
|
||||
return events.reduce((html, event) => {
|
||||
html += `<li component="topic/event" class="timeline-event" data-topic-event-id="${event.id}">
|
||||
<div class="timeline-badge">
|
||||
<i class="fa ${event.icon || 'fa-circle'}"></i>
|
||||
</div>
|
||||
<span class="timeline-text">
|
||||
${event.href ? `<a href="${relative_path}${event.href}">${event.text}</a>` : event.text}
|
||||
</span>
|
||||
`;
|
||||
|
||||
if (event.user) {
|
||||
if (!event.user.system) {
|
||||
html += `<span><a href="${relative_path}/user/${event.user.userslug}">${buildAvatar(event.user, 'xs', true)} ${event.user.username}</a></span> `;
|
||||
} else {
|
||||
html += `<span class="timeline-text">[[global:system-user]]</span> `;
|
||||
}
|
||||
}
|
||||
|
||||
html += `<span class="timeago timeline-text" title="${event.timestampISO}"></span>`;
|
||||
|
||||
if (this.privileges.isAdminOrMod) {
|
||||
html += ` <span component="topic/event/delete" data-topic-event-id="${event.id}" class="timeline-text pointer" title="[[topic:delete-event]]"><i class="fa fa-trash"></i></span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}, '');
|
||||
}
|
||||
|
||||
function renderDigestAvatar(block) {
|
||||
if (block.teaser) {
|
||||
if (block.teaser.user.picture) {
|
||||
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.teaser.user.picture + '" title="' + block.teaser.user.username + '" />';
|
||||
}
|
||||
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.teaser.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.teaser.user['icon:text'] + '</div>';
|
||||
}
|
||||
if (block.user.picture) {
|
||||
return '<img style="vertical-align: middle; width: 32px; height: 32px; border-radius: 50%;" src="' + block.user.picture + '" title="' + block.user.username + '" />';
|
||||
}
|
||||
return '<div style="vertical-align: middle; width: 32px; height: 32px; line-height: 32px; font-size: 16px; background-color: ' + block.user['icon:bgColor'] + '; color: white; text-align: center; display: inline-block; border-radius: 50%;">' + block.user['icon:text'] + '</div>';
|
||||
}
|
||||
|
||||
function userAgentIcons(data) {
|
||||
let icons = '';
|
||||
|
||||
switch (data.platform) {
|
||||
case 'Linux':
|
||||
icons += '<i class="fa fa-fw fa-linux"></i>';
|
||||
break;
|
||||
case 'Microsoft Windows':
|
||||
icons += '<i class="fa fa-fw fa-windows"></i>';
|
||||
break;
|
||||
case 'Apple Mac':
|
||||
icons += '<i class="fa fa-fw fa-apple"></i>';
|
||||
break;
|
||||
case 'Android':
|
||||
icons += '<i class="fa fa-fw fa-android"></i>';
|
||||
break;
|
||||
case 'iPad':
|
||||
icons += '<i class="fa fa-fw fa-tablet"></i>';
|
||||
break;
|
||||
case 'iPod': // intentional fall-through
|
||||
case 'iPhone':
|
||||
icons += '<i class="fa fa-fw fa-mobile"></i>';
|
||||
break;
|
||||
default:
|
||||
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
||||
break;
|
||||
}
|
||||
|
||||
switch (data.browser) {
|
||||
case 'Chrome':
|
||||
icons += '<i class="fa fa-fw fa-chrome"></i>';
|
||||
break;
|
||||
case 'Firefox':
|
||||
icons += '<i class="fa fa-fw fa-firefox"></i>';
|
||||
break;
|
||||
case 'Safari':
|
||||
icons += '<i class="fa fa-fw fa-safari"></i>';
|
||||
break;
|
||||
case 'IE':
|
||||
icons += '<i class="fa fa-fw fa-internet-explorer"></i>';
|
||||
break;
|
||||
case 'Edge':
|
||||
icons += '<i class="fa fa-fw fa-edge"></i>';
|
||||
break;
|
||||
default:
|
||||
icons += '<i class="fa fa-fw fa-question-circle"></i>';
|
||||
break;
|
||||
}
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
function buildAvatar(userObj, size, rounded, classNames, component) {
|
||||
/**
|
||||
* userObj requires:
|
||||
* - uid, picture, icon:bgColor, icon:text (getUserField w/ "picture" should return all 4), username
|
||||
* size: one of "xs", "sm", "md", "lg", or "xl" (required), or an integer
|
||||
* rounded: true or false (optional, default false)
|
||||
* classNames: additional class names to prepend (optional, default none)
|
||||
* component: overrides the default component (optional, default none)
|
||||
*/
|
||||
|
||||
// Try to use root context if passed-in userObj is undefined
|
||||
if (!userObj) {
|
||||
userObj = this;
|
||||
}
|
||||
|
||||
const attributes = [
|
||||
'alt="' + userObj.username + '"',
|
||||
'title="' + userObj.username + '"',
|
||||
'data-uid="' + userObj.uid + '"',
|
||||
'loading="lazy"',
|
||||
];
|
||||
const styles = [];
|
||||
classNames = classNames || '';
|
||||
|
||||
// Validate sizes, handle integers, otherwise fall back to `avatar-sm`
|
||||
if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) {
|
||||
classNames += ' avatar-' + size;
|
||||
} else if (!isNaN(parseInt(size, 10))) {
|
||||
styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (parseInt(size, 10) / 16) + 'rem;');
|
||||
} else {
|
||||
classNames += ' avatar-sm';
|
||||
}
|
||||
attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"');
|
||||
|
||||
// Component override
|
||||
if (component) {
|
||||
attributes.push('component="' + component + '"');
|
||||
} else {
|
||||
attributes.push('component="avatar/' + (userObj.picture ? 'picture' : 'icon') + '"');
|
||||
}
|
||||
|
||||
if (userObj.picture) {
|
||||
return '<img ' + attributes.join(' ') + ' src="' + userObj.picture + '" style="' + styles.join(' ') + '" />';
|
||||
}
|
||||
|
||||
styles.push('background-color: ' + userObj['icon:bgColor'] + ';');
|
||||
return '<span ' + attributes.join(' ') + ' style="' + styles.join(' ') + '">' + userObj['icon:text'] + '</span>';
|
||||
}
|
||||
|
||||
function register() {
|
||||
Object.keys(helpers).forEach(function (helperName) {
|
||||
Benchpress.registerHelper(helperName, helpers[helperName]);
|
||||
});
|
||||
}
|
||||
|
||||
return helpers;
|
||||
};
|
@ -0,0 +1,633 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function (utils, load, warn) {
|
||||
const assign = Object.assign || jQuery.extend;
|
||||
|
||||
function escapeHTML(str) {
|
||||
return utils.escapeHTML(utils.decodeHTMLEntities(
|
||||
String(str)
|
||||
.replace(/[\s\xa0]+/g, ' ')
|
||||
.replace(/^\s+|\s+$/g, '')
|
||||
));
|
||||
}
|
||||
|
||||
const Translator = (function () {
|
||||
/**
|
||||
* Construct a new Translator object
|
||||
* @param {string} language - Language code for this translator instance
|
||||
* @exports translator.Translator
|
||||
*/
|
||||
function Translator(language) {
|
||||
const self = this;
|
||||
|
||||
if (!language) {
|
||||
throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : ''));
|
||||
}
|
||||
|
||||
self.modules = Object.keys(Translator.moduleFactories).map(function (namespace) {
|
||||
const factory = Translator.moduleFactories[namespace];
|
||||
return [namespace, factory(language)];
|
||||
}).reduce(function (prev, elem) {
|
||||
const namespace = elem[0];
|
||||
const module = elem[1];
|
||||
prev[namespace] = module;
|
||||
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
self.lang = language;
|
||||
self.translations = {};
|
||||
}
|
||||
|
||||
Translator.prototype.load = load;
|
||||
|
||||
/**
|
||||
* Parse the translation instructions into the language of the Translator instance
|
||||
* @param {string} str - Source string
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
Translator.prototype.translate = function translate(str) {
|
||||
// regex for valid text in namespace / key
|
||||
const validText = 'a-zA-Z0-9\\-_.\\/';
|
||||
const validTextRegex = new RegExp('[' + validText + ']');
|
||||
const invalidTextRegex = new RegExp('[^' + validText + '\\]]');
|
||||
|
||||
// current cursor position
|
||||
let cursor = 0;
|
||||
// last break of the input string
|
||||
let lastBreak = 0;
|
||||
// length of the input string
|
||||
const len = str.length;
|
||||
// array to hold the promises for the translations
|
||||
// and the strings of untranslated text in between
|
||||
const toTranslate = [];
|
||||
|
||||
// to store the state of if we're currently in a top-level token for later
|
||||
let inToken = false;
|
||||
|
||||
// split a translator string into an array of tokens
|
||||
// but don't split by commas inside other translator strings
|
||||
function split(text) {
|
||||
const len = text.length;
|
||||
const arr = [];
|
||||
let i = 0;
|
||||
let brk = 0;
|
||||
let level = 0;
|
||||
|
||||
while (i + 2 <= len) {
|
||||
if (text[i] === '[' && text[i + 1] === '[') {
|
||||
level += 1;
|
||||
i += 1;
|
||||
} else if (text[i] === ']' && text[i + 1] === ']') {
|
||||
level -= 1;
|
||||
i += 1;
|
||||
} else if (level === 0 && text[i] === ',' && text[i - 1] !== '\\') {
|
||||
arr.push(text.slice(brk, i).trim());
|
||||
i += 1;
|
||||
brk = i;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
arr.push(text.slice(brk, i + 1).trim());
|
||||
return arr;
|
||||
}
|
||||
|
||||
// move to the first [[
|
||||
cursor = str.indexOf('[[', cursor);
|
||||
|
||||
// the loooop, we'll go to where the cursor
|
||||
// is equal to the length of the string since
|
||||
// slice doesn't include the ending index
|
||||
while (cursor + 2 <= len && cursor !== -1) {
|
||||
// split the string from the last break
|
||||
// to the character before the cursor
|
||||
// add that to the result array
|
||||
toTranslate.push(str.slice(lastBreak, cursor));
|
||||
// set the cursor position past the beginning
|
||||
// brackets of the translation string
|
||||
cursor += 2;
|
||||
// set the last break to our current
|
||||
// spot since we just broke the string
|
||||
lastBreak = cursor;
|
||||
// we're in a token now
|
||||
inToken = true;
|
||||
|
||||
// the current level of nesting of the translation strings
|
||||
let level = 0;
|
||||
let char0;
|
||||
let char1;
|
||||
// validating the current string is actually a translation
|
||||
let textBeforeColonFound = false;
|
||||
let colonFound = false;
|
||||
let textAfterColonFound = false;
|
||||
let commaAfterNameFound = false;
|
||||
|
||||
while (cursor + 2 <= len) {
|
||||
char0 = str[cursor];
|
||||
char1 = str[cursor + 1];
|
||||
// found some text after the double bracket,
|
||||
// so this is probably a translation string
|
||||
if (!textBeforeColonFound && validTextRegex.test(char0)) {
|
||||
textBeforeColonFound = true;
|
||||
cursor += 1;
|
||||
// found a colon, so this is probably a translation string
|
||||
} else if (textBeforeColonFound && !colonFound && char0 === ':') {
|
||||
colonFound = true;
|
||||
cursor += 1;
|
||||
// found some text after the colon,
|
||||
// so this is probably a translation string
|
||||
} else if (colonFound && !textAfterColonFound && validTextRegex.test(char0)) {
|
||||
textAfterColonFound = true;
|
||||
cursor += 1;
|
||||
} else if (textAfterColonFound && !commaAfterNameFound && char0 === ',') {
|
||||
commaAfterNameFound = true;
|
||||
cursor += 1;
|
||||
// a space or comma was found before the name
|
||||
// this isn't a translation string, so back out
|
||||
} else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) &&
|
||||
invalidTextRegex.test(char0)) {
|
||||
cursor += 1;
|
||||
lastBreak -= 2;
|
||||
// no longer in a token
|
||||
inToken = false;
|
||||
if (level > 0) {
|
||||
level -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
// if we're at the beginning of another translation string,
|
||||
// we're nested, so add to our level
|
||||
} else if (char0 === '[' && char1 === '[') {
|
||||
level += 1;
|
||||
cursor += 2;
|
||||
// if we're at the end of a translation string
|
||||
} else if (char0 === ']' && char1 === ']') {
|
||||
// if we're at the base level, then this is the end
|
||||
if (level === 0) {
|
||||
// so grab the name and args
|
||||
const currentSlice = str.slice(lastBreak, cursor);
|
||||
const result = split(currentSlice);
|
||||
const name = result[0];
|
||||
const args = result.slice(1);
|
||||
|
||||
// make a backup based on the raw string of the token
|
||||
// if there are arguments to the token
|
||||
let backup = '';
|
||||
if (args && args.length) {
|
||||
backup = this.translate(currentSlice);
|
||||
}
|
||||
// add the translation promise to the array
|
||||
toTranslate.push(this.translateKey(name, args, backup));
|
||||
// skip past the ending brackets
|
||||
cursor += 2;
|
||||
// set this as our last break
|
||||
lastBreak = cursor;
|
||||
// and we're no longer in a translation string,
|
||||
// so continue with the main loop
|
||||
inToken = false;
|
||||
break;
|
||||
}
|
||||
// otherwise we lower the level
|
||||
level -= 1;
|
||||
// and skip past the ending brackets
|
||||
cursor += 2;
|
||||
} else {
|
||||
// otherwise just move to the next character
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// skip to the next [[
|
||||
cursor = str.indexOf('[[', cursor);
|
||||
}
|
||||
|
||||
// ending string of source
|
||||
let last = str.slice(lastBreak);
|
||||
|
||||
// if we were mid-token, treat it as invalid
|
||||
if (inToken) {
|
||||
last = this.translate(last);
|
||||
}
|
||||
|
||||
// add the remaining text after the last translation string
|
||||
toTranslate.push(last);
|
||||
|
||||
// and return a promise for the concatenated translated string
|
||||
return Promise.all(toTranslate).then(function (translated) {
|
||||
return translated.join('');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates a specific key and array of arguments
|
||||
* @param {string} name - Translation key (ex. 'global:home')
|
||||
* @param {string[]} args - Arguments for `%1`, `%2`, etc
|
||||
* @param {string|Promise<string>} backup - Text to use in case the key can't be found
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
Translator.prototype.translateKey = function translateKey(name, args, backup) {
|
||||
const self = this;
|
||||
|
||||
const result = name.split(':', 2);
|
||||
const namespace = result[0];
|
||||
const key = result[1];
|
||||
|
||||
if (self.modules[namespace]) {
|
||||
return Promise.resolve(self.modules[namespace](key, args));
|
||||
}
|
||||
|
||||
if (namespace && result.length === 1) {
|
||||
return Promise.resolve('[[' + namespace + ']]');
|
||||
}
|
||||
|
||||
if (namespace && !key) {
|
||||
warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"');
|
||||
return Promise.resolve('[[' + namespace + ']]');
|
||||
}
|
||||
|
||||
const translation = this.getTranslation(namespace, key);
|
||||
return translation.then(function (translated) {
|
||||
// check if the translation is missing first
|
||||
if (!translated) {
|
||||
warn('Missing translation "' + name + '" for language "' + self.lang + '"');
|
||||
return backup || key;
|
||||
}
|
||||
|
||||
const argsToTranslate = args.map(function (arg) {
|
||||
return self.translate(escapeHTML(arg));
|
||||
});
|
||||
|
||||
return Promise.all(argsToTranslate).then(function (translatedArgs) {
|
||||
let out = translated;
|
||||
translatedArgs.forEach(function (arg, i) {
|
||||
let escaped = arg.replace(/%(?=\d)/g, '%').replace(/\\,/g, ',');
|
||||
// fix double escaped translation keys, see https://github.com/NodeBB/NodeBB/issues/9206
|
||||
escaped = escaped.replace(/&lsqb;/g, '[')
|
||||
.replace(/&rsqb;/g, ']');
|
||||
out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped);
|
||||
});
|
||||
return out;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Load translation file (or use a cached version), and optionally return the translation of a certain key
|
||||
* @param {string} namespace - The file name of the translation namespace
|
||||
* @param {string} [key] - The key of the specific translation to getJSON
|
||||
* @returns {Promise<{ [key: string]: string } | string>}
|
||||
*/
|
||||
Translator.prototype.getTranslation = function getTranslation(namespace, key) {
|
||||
let translation;
|
||||
if (!namespace) {
|
||||
warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : ''));
|
||||
translation = Promise.resolve({});
|
||||
} else {
|
||||
this.translations[namespace] = this.translations[namespace] ||
|
||||
this.load(this.lang, namespace).catch(function () { return {}; });
|
||||
translation = this.translations[namespace];
|
||||
}
|
||||
|
||||
if (key) {
|
||||
return translation.then(function (x) {
|
||||
if (typeof x[key] === 'string') return x[key];
|
||||
const keyParts = key.split('.');
|
||||
for (let i = 0; i <= keyParts.length; i++) {
|
||||
if (i === keyParts.length) {
|
||||
// default to trying to find key with the same name as parent or equal to empty string
|
||||
return x[keyParts[i - 1]] !== undefined ? x[keyParts[i - 1]] : x[''];
|
||||
}
|
||||
switch (typeof x[keyParts[i]]) {
|
||||
case 'object':
|
||||
x = x[keyParts[i]];
|
||||
break;
|
||||
case 'string':
|
||||
if (i === keyParts.length - 1) {
|
||||
return x[keyParts[i]];
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Node} node
|
||||
* @returns {Node[]}
|
||||
*/
|
||||
function descendantTextNodes(node) {
|
||||
const textNodes = [];
|
||||
|
||||
function helper(node) {
|
||||
if (node.nodeType === 3) {
|
||||
textNodes.push(node);
|
||||
} else {
|
||||
for (let i = 0, c = node.childNodes, l = c.length; i < l; i += 1) {
|
||||
helper(c[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
helper(node);
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively translate a DOM element in place
|
||||
* @param {Element} element - Root element to translate
|
||||
* @param {string[]} [attributes] - Array of node attributes to translate
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
Translator.prototype.translateInPlace = function translateInPlace(element, attributes) {
|
||||
attributes = attributes || ['placeholder', 'title'];
|
||||
|
||||
const nodes = descendantTextNodes(element);
|
||||
const text = nodes.map(function (node) {
|
||||
return utils.escapeHTML(node.nodeValue);
|
||||
}).join(' || ');
|
||||
|
||||
const attrNodes = attributes.reduce(function (prev, attr) {
|
||||
const tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) {
|
||||
return [attr, el];
|
||||
});
|
||||
return prev.concat(tuples);
|
||||
}, []);
|
||||
const attrText = attrNodes.map(function (node) {
|
||||
return node[1].getAttribute(node[0]);
|
||||
}).join(' || ');
|
||||
|
||||
return Promise.all([
|
||||
this.translate(text),
|
||||
this.translate(attrText),
|
||||
]).then(function (ref) {
|
||||
const translated = ref[0];
|
||||
const translatedAttrs = ref[1];
|
||||
if (translated) {
|
||||
translated.split(' || ').forEach(function (html, i) {
|
||||
$(nodes[i]).replaceWith(html);
|
||||
});
|
||||
}
|
||||
if (translatedAttrs) {
|
||||
translatedAttrs.split(' || ').forEach(function (text, i) {
|
||||
attrNodes[i][1].setAttribute(attrNodes[i][0], text);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the language of the current environment, falling back to defaults
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.getLanguage = function getLanguage() {
|
||||
return utils.getLanguage();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and cache a new Translator instance, or return a cached one
|
||||
* @param {string} [language] - ('en-GB') Language string
|
||||
* @returns {Translator}
|
||||
*/
|
||||
Translator.create = function create(language) {
|
||||
if (!language) {
|
||||
language = Translator.getLanguage();
|
||||
}
|
||||
|
||||
Translator.cache[language] = Translator.cache[language] || new Translator(language);
|
||||
|
||||
return Translator.cache[language];
|
||||
};
|
||||
|
||||
Translator.cache = {};
|
||||
|
||||
/**
|
||||
* Register a custom module to handle translations
|
||||
* @param {string} namespace - Namespace to handle translation for
|
||||
* @param {Function} factory - Function to return the translation function for this namespace
|
||||
*/
|
||||
Translator.registerModule = function registerModule(namespace, factory) {
|
||||
Translator.moduleFactories[namespace] = factory;
|
||||
|
||||
Object.keys(Translator.cache).forEach(function (key) {
|
||||
const translator = Translator.cache[key];
|
||||
translator.modules[namespace] = factory(translator.lang);
|
||||
});
|
||||
};
|
||||
|
||||
Translator.moduleFactories = {};
|
||||
|
||||
/**
|
||||
* Remove the translator patterns from text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.removePatterns = function removePatterns(text) {
|
||||
const len = text.length;
|
||||
let cursor = 0;
|
||||
let lastBreak = 0;
|
||||
let level = 0;
|
||||
let out = '';
|
||||
let sub;
|
||||
|
||||
while (cursor < len) {
|
||||
sub = text.slice(cursor, cursor + 2);
|
||||
if (sub === '[[') {
|
||||
if (level === 0) {
|
||||
out += text.slice(lastBreak, cursor);
|
||||
}
|
||||
level += 1;
|
||||
cursor += 2;
|
||||
} else if (sub === ']]') {
|
||||
level -= 1;
|
||||
cursor += 2;
|
||||
if (level === 0) {
|
||||
lastBreak = cursor;
|
||||
}
|
||||
} else {
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
out += text.slice(lastBreak, cursor);
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Escape translator patterns in text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.escape = function escape(text) {
|
||||
return typeof text === 'string' ? text.replace(/\[\[/g, '[[').replace(/\]\]/g, ']]') : text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unescape escaped translator patterns in text
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
Translator.unescape = function unescape(text) {
|
||||
return typeof text === 'string' ?
|
||||
text.replace(/[/g, '[').replace(/\\\[/g, '[')
|
||||
.replace(/]/g, ']').replace(/\\\]/g, ']') :
|
||||
text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a translator pattern
|
||||
* @param {string} name - Translation name
|
||||
* @param {...string} arg - Optional argument for the pattern
|
||||
*/
|
||||
Translator.compile = function compile() {
|
||||
const args = Array.prototype.slice.call(arguments, 0).map(function (text) {
|
||||
// escape commas and percent signs in arguments
|
||||
return String(text).replace(/%/g, '%').replace(/,/g, ',');
|
||||
});
|
||||
|
||||
return '[[' + args.join(', ') + ']]';
|
||||
};
|
||||
|
||||
return Translator;
|
||||
}());
|
||||
|
||||
/**
|
||||
* @exports translator
|
||||
*/
|
||||
const adaptor = {
|
||||
/**
|
||||
* The Translator class
|
||||
*/
|
||||
Translator: Translator,
|
||||
|
||||
compile: Translator.compile,
|
||||
escape: Translator.escape,
|
||||
unescape: Translator.unescape,
|
||||
getLanguage: Translator.getLanguage,
|
||||
|
||||
flush: function () {
|
||||
Object.keys(Translator.cache).forEach(function (code) {
|
||||
Translator.cache[code].translations = {};
|
||||
});
|
||||
},
|
||||
|
||||
flushNamespace: function (namespace) {
|
||||
Object.keys(Translator.cache).forEach(function (code) {
|
||||
if (Translator.cache[code] &&
|
||||
Translator.cache[code].translations &&
|
||||
Translator.cache[code].translations[namespace]
|
||||
) {
|
||||
Translator.cache[code].translations[namespace] = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Legacy translator function for backwards compatibility
|
||||
*/
|
||||
translate: function translate(text, language, callback) {
|
||||
// TODO: deprecate?
|
||||
|
||||
let cb = callback;
|
||||
let lang = language;
|
||||
if (typeof language === 'function') {
|
||||
cb = language;
|
||||
lang = null;
|
||||
}
|
||||
|
||||
if (!(typeof text === 'string' || text instanceof String) || text === '') {
|
||||
if (cb) {
|
||||
return setTimeout(cb, 0, '');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return Translator.create(lang).translate(text).then(function (output) {
|
||||
if (cb) {
|
||||
setTimeout(cb, 0, output);
|
||||
}
|
||||
return output;
|
||||
}, function (err) {
|
||||
warn('Translation failed: ' + err.stack);
|
||||
});
|
||||
},
|
||||
translateKeys: async function (keys, language, callback) {
|
||||
let cb = callback;
|
||||
let lang = language;
|
||||
if (typeof language === 'function') {
|
||||
cb = language;
|
||||
lang = null;
|
||||
}
|
||||
const translations = await Promise.all(keys.map(key => adaptor.translate(key, lang)));
|
||||
if (typeof cb === 'function') {
|
||||
return setTimeout(cb, 0, translations);
|
||||
}
|
||||
return translations;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add translations to the cache
|
||||
*/
|
||||
addTranslation: function addTranslation(language, namespace, translation) {
|
||||
Translator.create(language).getTranslation(namespace).then(function (translations) {
|
||||
assign(translations, translation);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the translations object
|
||||
*/
|
||||
getTranslations: function getTranslations(language, namespace, callback) {
|
||||
callback = callback || function () {};
|
||||
Translator.create(language).getTranslation(namespace).then(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Alias of getTranslations
|
||||
*/
|
||||
load: function load(language, namespace, callback) {
|
||||
adaptor.getTranslations(language, namespace, callback);
|
||||
},
|
||||
|
||||
toggleTimeagoShorthand: function toggleTimeagoShorthand(callback) {
|
||||
/* eslint "prefer-object-spread": "off" */
|
||||
function toggle() {
|
||||
const tmp = assign({}, jQuery.timeago.settings.strings);
|
||||
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
|
||||
adaptor.timeagoShort = assign({}, tmp);
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
if (!adaptor.timeagoShort) {
|
||||
let languageCode = utils.userLangToTimeagoCode(config.userLang);
|
||||
if (!config.timeagoCodes.includes(languageCode + '-short')) {
|
||||
languageCode = 'en';
|
||||
}
|
||||
|
||||
const originalSettings = assign({}, jQuery.timeago.settings.strings);
|
||||
adaptor.switchTimeagoLanguage(languageCode + '-short', function () {
|
||||
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
|
||||
jQuery.timeago.settings.strings = assign({}, originalSettings);
|
||||
toggle();
|
||||
});
|
||||
} else {
|
||||
toggle();
|
||||
}
|
||||
},
|
||||
|
||||
switchTimeagoLanguage: function switchTimeagoLanguage(langCode, callback) {
|
||||
// Delete the cached shorthand strings if present
|
||||
delete adaptor.timeagoShort;
|
||||
import(/* webpackChunkName: "timeago/[request]" */ 'timeago/locales/jquery.timeago.' + langCode).then(callback);
|
||||
},
|
||||
};
|
||||
|
||||
return adaptor;
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require.config({
|
||||
baseUrl: config.assetBaseUrl + '/src/modules',
|
||||
waitSeconds: 0,
|
||||
urlArgs: config['cache-buster'],
|
||||
paths: {
|
||||
forum: '../client',
|
||||
admin: '../admin',
|
||||
vendor: '../../vendor',
|
||||
plugins: '../../plugins',
|
||||
},
|
||||
});
|
@ -0,0 +1,759 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
// add default escape function for escaping HTML entities
|
||||
const escapeCharMap = Object.freeze({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'`': '`',
|
||||
'=': '=',
|
||||
});
|
||||
function replaceChar(c) {
|
||||
return escapeCharMap[c];
|
||||
}
|
||||
const escapeChars = /[&<>"'`=]/g;
|
||||
|
||||
const HTMLEntities = Object.freeze({
|
||||
amp: '&',
|
||||
gt: '>',
|
||||
lt: '<',
|
||||
quot: '"',
|
||||
apos: "'",
|
||||
AElig: 198,
|
||||
Aacute: 193,
|
||||
Acirc: 194,
|
||||
Agrave: 192,
|
||||
Aring: 197,
|
||||
Atilde: 195,
|
||||
Auml: 196,
|
||||
Ccedil: 199,
|
||||
ETH: 208,
|
||||
Eacute: 201,
|
||||
Ecirc: 202,
|
||||
Egrave: 200,
|
||||
Euml: 203,
|
||||
Iacute: 205,
|
||||
Icirc: 206,
|
||||
Igrave: 204,
|
||||
Iuml: 207,
|
||||
Ntilde: 209,
|
||||
Oacute: 211,
|
||||
Ocirc: 212,
|
||||
Ograve: 210,
|
||||
Oslash: 216,
|
||||
Otilde: 213,
|
||||
Ouml: 214,
|
||||
THORN: 222,
|
||||
Uacute: 218,
|
||||
Ucirc: 219,
|
||||
Ugrave: 217,
|
||||
Uuml: 220,
|
||||
Yacute: 221,
|
||||
aacute: 225,
|
||||
acirc: 226,
|
||||
aelig: 230,
|
||||
agrave: 224,
|
||||
aring: 229,
|
||||
atilde: 227,
|
||||
auml: 228,
|
||||
ccedil: 231,
|
||||
eacute: 233,
|
||||
ecirc: 234,
|
||||
egrave: 232,
|
||||
eth: 240,
|
||||
euml: 235,
|
||||
iacute: 237,
|
||||
icirc: 238,
|
||||
igrave: 236,
|
||||
iuml: 239,
|
||||
ntilde: 241,
|
||||
oacute: 243,
|
||||
ocirc: 244,
|
||||
ograve: 242,
|
||||
oslash: 248,
|
||||
otilde: 245,
|
||||
ouml: 246,
|
||||
szlig: 223,
|
||||
thorn: 254,
|
||||
uacute: 250,
|
||||
ucirc: 251,
|
||||
ugrave: 249,
|
||||
uuml: 252,
|
||||
yacute: 253,
|
||||
yuml: 255,
|
||||
copy: 169,
|
||||
reg: 174,
|
||||
nbsp: 160,
|
||||
iexcl: 161,
|
||||
cent: 162,
|
||||
pound: 163,
|
||||
curren: 164,
|
||||
yen: 165,
|
||||
brvbar: 166,
|
||||
sect: 167,
|
||||
uml: 168,
|
||||
ordf: 170,
|
||||
laquo: 171,
|
||||
not: 172,
|
||||
shy: 173,
|
||||
macr: 175,
|
||||
deg: 176,
|
||||
plusmn: 177,
|
||||
sup1: 185,
|
||||
sup2: 178,
|
||||
sup3: 179,
|
||||
acute: 180,
|
||||
micro: 181,
|
||||
para: 182,
|
||||
middot: 183,
|
||||
cedil: 184,
|
||||
ordm: 186,
|
||||
raquo: 187,
|
||||
frac14: 188,
|
||||
frac12: 189,
|
||||
frac34: 190,
|
||||
iquest: 191,
|
||||
times: 215,
|
||||
divide: 247,
|
||||
'OElig;': 338,
|
||||
'oelig;': 339,
|
||||
'Scaron;': 352,
|
||||
'scaron;': 353,
|
||||
'Yuml;': 376,
|
||||
'fnof;': 402,
|
||||
'circ;': 710,
|
||||
'tilde;': 732,
|
||||
'Alpha;': 913,
|
||||
'Beta;': 914,
|
||||
'Gamma;': 915,
|
||||
'Delta;': 916,
|
||||
'Epsilon;': 917,
|
||||
'Zeta;': 918,
|
||||
'Eta;': 919,
|
||||
'Theta;': 920,
|
||||
'Iota;': 921,
|
||||
'Kappa;': 922,
|
||||
'Lambda;': 923,
|
||||
'Mu;': 924,
|
||||
'Nu;': 925,
|
||||
'Xi;': 926,
|
||||
'Omicron;': 927,
|
||||
'Pi;': 928,
|
||||
'Rho;': 929,
|
||||
'Sigma;': 931,
|
||||
'Tau;': 932,
|
||||
'Upsilon;': 933,
|
||||
'Phi;': 934,
|
||||
'Chi;': 935,
|
||||
'Psi;': 936,
|
||||
'Omega;': 937,
|
||||
'alpha;': 945,
|
||||
'beta;': 946,
|
||||
'gamma;': 947,
|
||||
'delta;': 948,
|
||||
'epsilon;': 949,
|
||||
'zeta;': 950,
|
||||
'eta;': 951,
|
||||
'theta;': 952,
|
||||
'iota;': 953,
|
||||
'kappa;': 954,
|
||||
'lambda;': 955,
|
||||
'mu;': 956,
|
||||
'nu;': 957,
|
||||
'xi;': 958,
|
||||
'omicron;': 959,
|
||||
'pi;': 960,
|
||||
'rho;': 961,
|
||||
'sigmaf;': 962,
|
||||
'sigma;': 963,
|
||||
'tau;': 964,
|
||||
'upsilon;': 965,
|
||||
'phi;': 966,
|
||||
'chi;': 967,
|
||||
'psi;': 968,
|
||||
'omega;': 969,
|
||||
'thetasym;': 977,
|
||||
'upsih;': 978,
|
||||
'piv;': 982,
|
||||
'ensp;': 8194,
|
||||
'emsp;': 8195,
|
||||
'thinsp;': 8201,
|
||||
'zwnj;': 8204,
|
||||
'zwj;': 8205,
|
||||
'lrm;': 8206,
|
||||
'rlm;': 8207,
|
||||
'ndash;': 8211,
|
||||
'mdash;': 8212,
|
||||
'lsquo;': 8216,
|
||||
'rsquo;': 8217,
|
||||
'sbquo;': 8218,
|
||||
'ldquo;': 8220,
|
||||
'rdquo;': 8221,
|
||||
'bdquo;': 8222,
|
||||
'dagger;': 8224,
|
||||
'Dagger;': 8225,
|
||||
'bull;': 8226,
|
||||
'hellip;': 8230,
|
||||
'permil;': 8240,
|
||||
'prime;': 8242,
|
||||
'Prime;': 8243,
|
||||
'lsaquo;': 8249,
|
||||
'rsaquo;': 8250,
|
||||
'oline;': 8254,
|
||||
'frasl;': 8260,
|
||||
'euro;': 8364,
|
||||
'image;': 8465,
|
||||
'weierp;': 8472,
|
||||
'real;': 8476,
|
||||
'trade;': 8482,
|
||||
'alefsym;': 8501,
|
||||
'larr;': 8592,
|
||||
'uarr;': 8593,
|
||||
'rarr;': 8594,
|
||||
'darr;': 8595,
|
||||
'harr;': 8596,
|
||||
'crarr;': 8629,
|
||||
'lArr;': 8656,
|
||||
'uArr;': 8657,
|
||||
'rArr;': 8658,
|
||||
'dArr;': 8659,
|
||||
'hArr;': 8660,
|
||||
'forall;': 8704,
|
||||
'part;': 8706,
|
||||
'exist;': 8707,
|
||||
'empty;': 8709,
|
||||
'nabla;': 8711,
|
||||
'isin;': 8712,
|
||||
'notin;': 8713,
|
||||
'ni;': 8715,
|
||||
'prod;': 8719,
|
||||
'sum;': 8721,
|
||||
'minus;': 8722,
|
||||
'lowast;': 8727,
|
||||
'radic;': 8730,
|
||||
'prop;': 8733,
|
||||
'infin;': 8734,
|
||||
'ang;': 8736,
|
||||
'and;': 8743,
|
||||
'or;': 8744,
|
||||
'cap;': 8745,
|
||||
'cup;': 8746,
|
||||
'int;': 8747,
|
||||
'there4;': 8756,
|
||||
'sim;': 8764,
|
||||
'cong;': 8773,
|
||||
'asymp;': 8776,
|
||||
'ne;': 8800,
|
||||
'equiv;': 8801,
|
||||
'le;': 8804,
|
||||
'ge;': 8805,
|
||||
'sub;': 8834,
|
||||
'sup;': 8835,
|
||||
'nsub;': 8836,
|
||||
'sube;': 8838,
|
||||
'supe;': 8839,
|
||||
'oplus;': 8853,
|
||||
'otimes;': 8855,
|
||||
'perp;': 8869,
|
||||
'sdot;': 8901,
|
||||
'lceil;': 8968,
|
||||
'rceil;': 8969,
|
||||
'lfloor;': 8970,
|
||||
'rfloor;': 8971,
|
||||
'lang;': 9001,
|
||||
'rang;': 9002,
|
||||
'loz;': 9674,
|
||||
'spades;': 9824,
|
||||
'clubs;': 9827,
|
||||
'hearts;': 9829,
|
||||
'diams;': 9830,
|
||||
});
|
||||
|
||||
/* eslint-disable no-redeclare */
|
||||
const utils = {
|
||||
generateUUID: function () {
|
||||
/* eslint-disable no-bitwise */
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : ((r & 0x3) | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
/* eslint-enable no-bitwise */
|
||||
},
|
||||
// https://github.com/substack/node-ent/blob/master/index.js
|
||||
decodeHTMLEntities: function (html) {
|
||||
return String(html)
|
||||
.replace(/&#(\d+);?/g, function (_, code) {
|
||||
return String.fromCharCode(code);
|
||||
})
|
||||
.replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
})
|
||||
.replace(/&([^;\W]+;?)/g, function (m, e) {
|
||||
const ee = e.replace(/;$/, '');
|
||||
const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]);
|
||||
|
||||
if (typeof target === 'number') {
|
||||
return String.fromCharCode(target);
|
||||
} else if (typeof target === 'string') {
|
||||
return target;
|
||||
}
|
||||
|
||||
return m;
|
||||
});
|
||||
},
|
||||
// https://github.com/jprichardson/string.js/blob/master/lib/string.js
|
||||
stripHTMLTags: function (str, tags) {
|
||||
const pattern = (tags || ['']).join('|');
|
||||
return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), '');
|
||||
},
|
||||
|
||||
cleanUpTag: function (tag, maxLength) {
|
||||
if (typeof tag !== 'string' || !tag.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
tag = tag.trim().toLowerCase();
|
||||
// see https://github.com/NodeBB/NodeBB/issues/4378
|
||||
tag = tag.replace(/\u202E/gi, '');
|
||||
tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, '');
|
||||
tag = tag.slice(0, maxLength || 15).trim();
|
||||
const matches = tag.match(/^[.-]*(.+?)[.-]*$/);
|
||||
if (matches && matches.length > 1) {
|
||||
tag = matches[1];
|
||||
}
|
||||
return tag;
|
||||
},
|
||||
|
||||
removePunctuation: function (str) {
|
||||
return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, '');
|
||||
},
|
||||
|
||||
isEmailValid: function (email) {
|
||||
return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1;
|
||||
},
|
||||
|
||||
isUserNameValid: function (name) {
|
||||
return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name)));
|
||||
},
|
||||
|
||||
isPasswordValid: function (password) {
|
||||
return typeof password === 'string' && password.length;
|
||||
},
|
||||
|
||||
isNumber: function (n) {
|
||||
// `isFinite('') === true` so isNan parseFloat check is necessary
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
},
|
||||
|
||||
languageKeyRegex: /\[\[[\w]+:.+\]\]/,
|
||||
hasLanguageKey: function (input) {
|
||||
return utils.languageKeyRegex.test(input);
|
||||
},
|
||||
userLangToTimeagoCode: function (userLang) {
|
||||
const mapping = {
|
||||
'en-GB': 'en',
|
||||
'en-US': 'en',
|
||||
'fa-IR': 'fa',
|
||||
'pt-BR': 'pt-br',
|
||||
nb: 'no',
|
||||
};
|
||||
return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang;
|
||||
},
|
||||
// shallow objects merge
|
||||
merge: function () {
|
||||
const result = {};
|
||||
let obj;
|
||||
let keys;
|
||||
for (let i = 0; i < arguments.length; i += 1) {
|
||||
obj = arguments[i] || {};
|
||||
keys = Object.keys(obj);
|
||||
for (let j = 0; j < keys.length; j += 1) {
|
||||
result[keys[j]] = obj[keys[j]];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
fileExtension: function (path) {
|
||||
return ('' + path).split('.').pop();
|
||||
},
|
||||
|
||||
extensionMimeTypeMap: {
|
||||
bmp: 'image/bmp',
|
||||
cmx: 'image/x-cmx',
|
||||
cod: 'image/cis-cod',
|
||||
gif: 'image/gif',
|
||||
ico: 'image/x-icon',
|
||||
ief: 'image/ief',
|
||||
jfif: 'image/pipeg',
|
||||
jpe: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
pbm: 'image/x-portable-bitmap',
|
||||
pgm: 'image/x-portable-graymap',
|
||||
pnm: 'image/x-portable-anymap',
|
||||
ppm: 'image/x-portable-pixmap',
|
||||
ras: 'image/x-cmu-raster',
|
||||
rgb: 'image/x-rgb',
|
||||
svg: 'image/svg+xml',
|
||||
tif: 'image/tiff',
|
||||
tiff: 'image/tiff',
|
||||
xbm: 'image/x-xbitmap',
|
||||
xpm: 'image/x-xpixmap',
|
||||
xwd: 'image/x-xwindowdump',
|
||||
},
|
||||
|
||||
fileMimeType: function (path) {
|
||||
return utils.extensionToMimeType(utils.fileExtension(path));
|
||||
},
|
||||
|
||||
extensionToMimeType: function (extension) {
|
||||
return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*';
|
||||
},
|
||||
|
||||
isPromise: function (object) {
|
||||
// https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324
|
||||
return object && typeof object.then === 'function';
|
||||
},
|
||||
|
||||
promiseParallel: function (obj) {
|
||||
const keys = Object.keys(obj);
|
||||
return Promise.all(
|
||||
keys.map(function (k) { return obj[k]; })
|
||||
).then(function (results) {
|
||||
const data = {};
|
||||
keys.forEach(function (k, i) {
|
||||
data[k] = results[i];
|
||||
});
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
// https://github.com/sindresorhus/is-absolute-url
|
||||
isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/,
|
||||
isWinPathRE: /^[a-zA-Z]:\\/,
|
||||
isAbsoluteUrl: function (url) {
|
||||
if (utils.isWinPathRE.test(url)) {
|
||||
return false;
|
||||
}
|
||||
return utils.isAbsoluteUrlRE.test(url);
|
||||
},
|
||||
|
||||
isRelativeUrl: function (url) {
|
||||
return !utils.isAbsoluteUrl(url);
|
||||
},
|
||||
|
||||
makeNumberHumanReadable: function (num) {
|
||||
const n = parseInt(num, 10);
|
||||
if (!n) {
|
||||
return num;
|
||||
}
|
||||
if (n > 999999) {
|
||||
return (n / 1000000).toFixed(1) + 'm';
|
||||
} else if (n > 999) {
|
||||
return (n / 1000).toFixed(1) + 'k';
|
||||
}
|
||||
return n;
|
||||
},
|
||||
|
||||
// takes a string like 1000 and returns 1,000
|
||||
addCommas: function (text) {
|
||||
return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
|
||||
},
|
||||
|
||||
toISOString: function (timestamp) {
|
||||
if (!timestamp || !Date.prototype.toISOString) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Prevent too-high values to be passed to Date object
|
||||
timestamp = Math.min(timestamp, 8640000000000000);
|
||||
|
||||
try {
|
||||
return new Date(parseInt(timestamp, 10)).toISOString();
|
||||
} catch (e) {
|
||||
return timestamp;
|
||||
}
|
||||
},
|
||||
|
||||
tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont',
|
||||
'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
||||
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
||||
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
||||
'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
||||
'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
||||
'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
||||
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
||||
|
||||
stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont',
|
||||
'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup',
|
||||
'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed',
|
||||
'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link',
|
||||
'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option',
|
||||
'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
|
||||
'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot',
|
||||
'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'],
|
||||
|
||||
escapeRegexChars: function (text) {
|
||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||
},
|
||||
|
||||
escapeHTML: function (str) {
|
||||
if (str == null) {
|
||||
return '';
|
||||
}
|
||||
if (!str) {
|
||||
return String(str);
|
||||
}
|
||||
|
||||
return str.toString().replace(escapeChars, replaceChar);
|
||||
},
|
||||
|
||||
isAndroidBrowser: function () {
|
||||
// http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser
|
||||
const nua = navigator.userAgent;
|
||||
return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1));
|
||||
},
|
||||
|
||||
isTouchDevice: function () {
|
||||
return 'ontouchstart' in document.documentElement;
|
||||
},
|
||||
|
||||
findBootstrapEnvironment: function () {
|
||||
// http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api
|
||||
const envs = ['xs', 'sm', 'md', 'lg'];
|
||||
const $el = $('<div>');
|
||||
|
||||
$el.appendTo($('body'));
|
||||
|
||||
for (let i = envs.length - 1; i >= 0; i -= 1) {
|
||||
const env = envs[i];
|
||||
|
||||
$el.addClass('hidden-' + env);
|
||||
if ($el.is(':hidden')) {
|
||||
$el.remove();
|
||||
return env;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isMobile: function () {
|
||||
const env = utils.findBootstrapEnvironment();
|
||||
return ['xs', 'sm'].some(function (targetEnv) {
|
||||
return targetEnv === env;
|
||||
});
|
||||
},
|
||||
|
||||
getHoursArray: function () {
|
||||
const currentHour = new Date().getHours();
|
||||
const labels = [];
|
||||
|
||||
for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) {
|
||||
const hour = i < 0 ? 24 + i : i;
|
||||
labels.push(hour + ':00');
|
||||
}
|
||||
|
||||
return labels.reverse();
|
||||
},
|
||||
|
||||
getDaysArray: function (from, amount) {
|
||||
const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime();
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const labels = [];
|
||||
let tmpDate;
|
||||
|
||||
for (let x = (amount || 30) - 1; x >= 0; x -= 1) {
|
||||
tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x));
|
||||
labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate());
|
||||
}
|
||||
|
||||
return labels;
|
||||
},
|
||||
|
||||
/* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */
|
||||
isElementInViewport: function (el) {
|
||||
// special bonus for those using jQuery
|
||||
if (typeof jQuery === 'function' && el instanceof jQuery) {
|
||||
el = el[0];
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */
|
||||
);
|
||||
},
|
||||
|
||||
// get all the url params in a single key/value hash
|
||||
params: function (options = {}) {
|
||||
let url;
|
||||
if (options.url && !options.url.startsWith('http')) {
|
||||
// relative path passed in
|
||||
options.url = options.url.replace(new RegExp(`/?${config.relative_path.slice(1)}/`, 'g'), '');
|
||||
url = new URL(document.location);
|
||||
url.pathname = options.url;
|
||||
} else {
|
||||
url = new URL(options.url || document.location);
|
||||
}
|
||||
let params = url.searchParams;
|
||||
|
||||
if (options.full) { // return URLSearchParams object
|
||||
return params;
|
||||
}
|
||||
|
||||
// Handle arrays passed in query string (Object.fromEntries does not)
|
||||
const arrays = {};
|
||||
params.forEach((value, key) => {
|
||||
if (!key.endsWith('[]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
key = key.slice(0, -2);
|
||||
arrays[key] = arrays[key] || [];
|
||||
arrays[key].push(utils.toType(value));
|
||||
});
|
||||
Object.keys(arrays).forEach((key) => {
|
||||
params.delete(`${key}[]`);
|
||||
});
|
||||
|
||||
// Backwards compatibility with v1.x -- all values passed through utils.toType()
|
||||
params = Object.fromEntries(params);
|
||||
Object.keys(params).forEach((key) => {
|
||||
params[key] = utils.toType(params[key]);
|
||||
});
|
||||
|
||||
return { ...params, ...arrays };
|
||||
},
|
||||
|
||||
param: function (key) {
|
||||
return this.params()[key];
|
||||
},
|
||||
|
||||
urlToLocation: function (url) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
return a;
|
||||
},
|
||||
|
||||
// return boolean if string 'true' or string 'false', or if a parsable string which is a number
|
||||
// also supports JSON object and/or arrays parsing
|
||||
toType: function (str) {
|
||||
const type = typeof str;
|
||||
if (type !== 'string') {
|
||||
return str;
|
||||
}
|
||||
const nb = parseFloat(str);
|
||||
if (!isNaN(nb) && isFinite(str)) {
|
||||
return nb;
|
||||
}
|
||||
if (str === 'false') {
|
||||
return false;
|
||||
}
|
||||
if (str === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
str = JSON.parse(str);
|
||||
} catch (e) {}
|
||||
|
||||
return str;
|
||||
},
|
||||
|
||||
// Safely get/set chained properties on an object
|
||||
// set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10
|
||||
// get example: utils.props(A, 'a.b.c') // returns {d: 10}
|
||||
// get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError
|
||||
// credits to github.com/gkindel
|
||||
props: function (obj, props, value) {
|
||||
if (obj === undefined) {
|
||||
obj = window;
|
||||
}
|
||||
if (props == null) {
|
||||
return undefined;
|
||||
}
|
||||
const i = props.indexOf('.');
|
||||
if (i === -1) {
|
||||
if (value !== undefined) {
|
||||
obj[props] = value;
|
||||
}
|
||||
return obj[props];
|
||||
}
|
||||
const prop = props.slice(0, i);
|
||||
const newProps = props.slice(i + 1);
|
||||
|
||||
if (props !== undefined && !(obj[prop] instanceof Object)) {
|
||||
obj[prop] = {};
|
||||
}
|
||||
|
||||
return utils.props(obj[prop], newProps, value);
|
||||
},
|
||||
|
||||
isInternalURI: function (targetLocation, referenceLocation, relative_path) {
|
||||
return targetLocation.host === '' || // Relative paths are always internal links
|
||||
(
|
||||
targetLocation.host === referenceLocation.host &&
|
||||
// Otherwise need to check if protocol and host match
|
||||
targetLocation.protocol === referenceLocation.protocol &&
|
||||
// Subfolder installs need this additional check
|
||||
(relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true)
|
||||
);
|
||||
},
|
||||
|
||||
rtrim: function (str) {
|
||||
return str.replace(/\s+$/g, '');
|
||||
},
|
||||
|
||||
debounce: function (func, wait, immediate) {
|
||||
// modified from https://davidwalsh.name/javascript-debounce-function
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
},
|
||||
throttle: function (func, wait, immediate) {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
const callNow = immediate && !timeout;
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(later, wait);
|
||||
}
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = utils;
|
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('../public/src/modules/helpers.common')(
|
||||
require('./utils'),
|
||||
require('benchpressjs'),
|
||||
require('nconf').get('relative_path'),
|
||||
);
|
@ -1,3 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('../public/src/modules/translator');
|
||||
const winston = require('winston');
|
||||
|
||||
function warn(msg) {
|
||||
winston.warn(msg);
|
||||
}
|
||||
|
||||
module.exports = require('../public/src/modules/translator.common')(require('./utils'), (lang, namespace) => {
|
||||
const languages = require('./languages');
|
||||
return languages.get(lang, namespace);
|
||||
}, warn);
|
||||
|
@ -1,3 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('../public/src/utils');
|
||||
process.profile = function (operation, start) {
|
||||
console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start));
|
||||
};
|
||||
|
||||
process.elapsedTimeSince = function (start) {
|
||||
const diff = process.hrtime(start);
|
||||
return (diff[0] * 1e3) + (diff[1] / 1e6);
|
||||
};
|
||||
const utils = require('../public/src/utils.common');
|
||||
|
||||
utils.getLanguage = function () {
|
||||
const meta = require('./meta');
|
||||
return meta.config && meta.config.defaultLang ? meta.config.defaultLang : 'en-GB';
|
||||
};
|
||||
module.exports = utils;
|
||||
|
@ -1,14 +0,0 @@
|
||||
<script>
|
||||
window.addEventListener('load', function () {
|
||||
define(config.relative_path + '/assets/templates/500.js', function () {
|
||||
function compiled(helpers, context, get, iter, helper) {
|
||||
return '<div class="alert alert-danger">\n\t<strong>[[global:500.title]]</strong>\n\t<p>[[global:500.message]]</p>\n\t<p>' +
|
||||
helpers.__escape(get(context && context['path'])) + '</p>\n\t' +
|
||||
(get(context && context['error']) ? '<p>' + helpers.__escape(get(context && context['error'])) + '</p>' : '') + '\n\n\t' +
|
||||
(get(context && context['returnLink']) ? '\n\t<p>[[error:goback]]</p>\n\t' : '') + '\n</div>\n';
|
||||
}
|
||||
|
||||
return compiled;
|
||||
});
|
||||
});
|
||||
</script>
|
@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const url = require('url');
|
||||
const nconf = require('nconf');
|
||||
|
||||
const activePlugins = require('./build/active_plugins.json');
|
||||
|
||||
let relativePath = nconf.get('relative_path');
|
||||
if (relativePath === undefined) {
|
||||
nconf.file({
|
||||
file: path.resolve(__dirname, nconf.any(['config', 'CONFIG']) || 'config.json'),
|
||||
});
|
||||
|
||||
const urlObject = url.parse(nconf.get('url'));
|
||||
relativePath = urlObject.pathname !== '/' ? urlObject.pathname.replace(/\/+$/, '') : '';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
plugins: [],
|
||||
entry: {
|
||||
nodebb: './build/public/src/client.js',
|
||||
admin: './build/public/src/admin/admin.js',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].min.js',
|
||||
chunkFilename: '[name].[contenthash].min.js',
|
||||
path: path.resolve(__dirname, 'build/public'),
|
||||
publicPath: `${relativePath}/assets/`,
|
||||
clean: {
|
||||
keep(asset) {
|
||||
return !asset.endsWith('.min.js');
|
||||
},
|
||||
},
|
||||
},
|
||||
watchOptions: {
|
||||
poll: 500,
|
||||
aggregateTimeout: 250,
|
||||
},
|
||||
resolve: {
|
||||
symlinks: false,
|
||||
modules: [
|
||||
'build/public/src/modules',
|
||||
'build/public/src',
|
||||
'node_modules',
|
||||
...activePlugins.map(p => `node_modules/${p}/node_modules`),
|
||||
],
|
||||
alias: {
|
||||
assets: path.resolve(__dirname, 'build/public'),
|
||||
forum: path.resolve(__dirname, 'build/public/src/client'),
|
||||
admin: path.resolve(__dirname, 'build/public/src/admin'),
|
||||
vendor: path.resolve(__dirname, 'public/vendor'),
|
||||
benchpress: path.resolve(__dirname, 'node_modules/benchpressjs'),
|
||||
Chart: path.resolve(__dirname, 'node_modules/chart.js'),
|
||||
Sortable: path.resolve(__dirname, 'node_modules/sortablejs'),
|
||||
cropper: path.resolve(__dirname, 'node_modules/cropperjs'),
|
||||
'jquery-ui/widgets': path.resolve(__dirname, 'node_modules/jquery-ui/ui/widgets'),
|
||||
'ace/ace': path.resolve(__dirname, 'build/public/src/modules/ace-editor.js'),
|
||||
},
|
||||
},
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
// devtool: 'inline-source-map',
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
// webpack config for webinstaller
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: {
|
||||
installer: './public/src/installer/install.js',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].min.js',
|
||||
path: path.resolve(__dirname, 'build/public'),
|
||||
publicPath: `/assets/`,
|
||||
},
|
||||
resolve: {
|
||||
symlinks: false,
|
||||
modules: [
|
||||
'public/src',
|
||||
'node_modules',
|
||||
],
|
||||
},
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { merge } = require('webpack-merge');
|
||||
const common = require('./webpack.common');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production',
|
||||
});
|
Loading…
Reference in New Issue