'use strict';
// External dependencies
const hbs = require('hbs');
const escapeHTML = require('escape-html');
const i18n = require('i18n');
const stripTags = require('striptags');
const linkifyHTML = require('linkify-html');
// Internal dependencies
const mlString = require('../dal/lib/ml-string');
const languages = require('../locales/languages');
const thingModelHandle = require('../models/thing');
const urlUtils = require('./url-utils');
const adapters = require('../adapters/adapters');
const getLicenseURL = require('./get-license-url');
const debug = require('./debug');
/**
* Resolve a thing's display label in the current locale, with safe fallbacks.
*
* @param {Object} thing - Thing object or plain data used by the template
* @param {string} locale - Active locale for rendering
* @returns {string} Localized label, prettified URL, or an empty string
*/
function getThingLabel(thing, locale) {
if (!thing) {
return '';
}
const modelGetLabel = thingModelHandle && thingModelHandle.getLabel;
if (typeof modelGetLabel === 'function') {
try {
const label = modelGetLabel(thing, locale);
if (label) {
return label;
}
} catch (err) {
debug && debug.error && debug.error('Failed to resolve thing label via model handle', err);
}
}
// Manual fallback mirrors Thing.getLabel behaviour without relying on the model
let resolved;
if (thing.label) {
resolved = mlString.resolve(locale, thing.label);
if (resolved && resolved.str) {
return resolved.str;
}
}
if (thing.urls && thing.urls.length) {
return urlUtils.prettify(thing.urls[0]);
}
return '';
}
// Current iteration value will be passed as {{this}} into the block,
// starts at 1 for more human-readable counts. First and last set @first, @last
hbs.registerHelper('times', function(n, block) {
let data = {},
rv = '';
if (block.data)
data = hbs.handlebars.createFrame(block.data);
for (let i = 1; i <= n; i++) {
data.zeroIndex = i - 1;
data.first = i == 1 ? true : false;
data.last = i == n ? true : false;
rv += block.fn(i, {
data
}).trim();
}
return rv;
});
hbs.registerHelper('escapeHTML', function(block) {
return escapeHTML(block.fn(this));
});
hbs.registerHelper('link', function(url, title, singleQuotes) {
let q = singleQuotes ? "'" : '"';
return `<a href=${q}${url}${q}>${title}</a>`;
});
// Strips HTML and shortens to specified length
hbs.registerHelper('summarize', function(html, length) {
let stripped = stripTags(html);
let shortened = stripped.substr(0, length);
if (stripped.length > length)
shortened += '...';
return shortened;
});
hbs.registerHelper('userLink', userLink);
hbs.registerHelper('prettify', function(url) {
if (url)
return urlUtils.prettify(url);
else
return '';
});
hbs.registerHelper('shortDate', function(date) {
if (date && date instanceof Date)
return date.toLocaleDateString();
});
hbs.registerHelper('longDate', function(date) {
if (!date)
return;
const value = date instanceof Date ? date : new Date(date);
if (value instanceof Date && !Number.isNaN(value.getTime()))
return value.toLocaleString();
});
// Sources are external sites we interface with; if they're known sources,
// they have a message key of this standard format.
hbs.registerHelper('getSourceMsgKey', function(sourceID) {
return `${sourceID} source label`;
});
// Licensing notices for sources follow a similar pattern
hbs.registerHelper('getSourceLicensingKey', function(sourceID) {
return `${sourceID} license`;
});
// Tags are used to classify sources into domains like "Databases"; these, too,
// have message keys.
hbs.registerHelper('getTagMsgKey', function(sourceID) {
return `${sourceID} tag label`;
});
hbs.registerHelper('getSourceURL', function(sourceID) {
return adapters.getSourceURL(sourceID);
});
hbs.registerHelper('__', function(...args) {
let options = args.pop();
return Reflect.apply(i18n.__, options.data.root, args);
});
hbs.registerHelper('__n', function(...args) {
let options = args.pop();
return Reflect.apply(i18n.__n, options.data.root, args);
});
// Get the language code that will result from resolving a string to the
// current request language (may be a fallback if no translation available).
hbs.registerHelper('getLang', function(str, options) {
let mlRv = mlString.resolve(options.data.root.locale, str);
return mlRv ? mlRv.lang : undefined;
});
hbs.registerHelper('getThingLabel', (thing, options) => getThingLabel(thing, options.data.root.locale));
// Just a simple %1, %2 substitution function for various purposes
hbs.registerHelper('substitute', function(...args) {
let i = 1,
string = args.shift();
while (args.length) {
let sub = args.shift();
string = string.replace(`%${i}`, sub);
i++;
}
return string;
});
hbs.registerHelper('getThingLink', (thing, options) => {
if (!thing) {
return '';
}
const label = getThingLabel(thing, options.data.root.locale);
return `<a href="/${thing.urlID}">${label || ''}</a>`;
});
// Filenames cannot contain HTML metacharacters, so URL encoding is sufficient here
hbs.registerHelper('getFileLink', filename => `<a href="/static/uploads/${encodeURIComponent(filename)}">${filename}</a>`);
hbs.registerHelper('getLanguageName', (lang, options) => languages.getTranslatedName(lang, options.data.root.locale));
hbs.registerHelper('isoDate', date => date && date.toISOString ? date.toISOString() : undefined);
// Resolve a multilingual string to the current request language.
//
// addLanguageSpan -- Do we want a little label next to the string (default true!)
hbs.registerHelper('mlString', function(str, addLanguageSpan, options) {
// hbs passes options object in as last parameter
if (arguments.length == 2) {
options = addLanguageSpan;
addLanguageSpan = true;
} else if (addLanguageSpan === undefined)
addLanguageSpan = true;
let mlRv = mlString.resolve(options.data.root.locale, str);
if (mlRv === undefined || mlRv.str === undefined || mlRv.str === '')
return undefined;
// Note that we don't show the label if we can't identify the language ('und')
if (!addLanguageSpan || mlRv.lang === options.data.root.locale || mlRv.lang == 'und')
return mlRv.str;
else {
let languageName = languages.getCompositeName(mlRv.lang, options.data.root.locale);
return `${mlRv.str} <span class="language-identifier" title="${languageName}">` +
`<span class="fa fa-fw fa-globe language-identifier-icon"> </span>${mlRv.lang}</span>`;
}
});
hbs.registerHelper('round', (num, dec) => +num.toFixed(dec));
hbs.registerHelper('ifCond', function(v1, operator, v2, options) {
switch (operator) {
case '==':
return v1 == v2 ? options.fn(this) : options.inverse(this);
case '===':
return v1 === v2 ? options.fn(this) : options.inverse(this);
case '<':
return v1 < v2 ? options.fn(this) : options.inverse(this);
case '<=':
return v1 <= v2 ? options.fn(this) : options.inverse(this);
case '>':
return v1 > v2 ? options.fn(this) : options.inverse(this);
case '>=':
return v1 >= v2 ? options.fn(this) : options.inverse(this);
case '&&':
return v1 && v2 ? options.fn(this) : options.inverse(this);
case '||':
return v1 || v2 ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
});
hbs.registerHelper('renderFilePreview', function(file, restricted) {
const path = restricted ? 'restricted/' : '';
const wrap = str => `<div class="file-preview">${str}</div>`;
if (/^image\//.test(file.mimeType))
return wrap(`<img src="/static/uploads/${path}${file.name}">`);
else if (/^video\//.test(file.mimeType))
return wrap(`<video src="/static/uploads/${path}${file.name}" controls>`);
else if (/^audio\//.test(file.mimeType))
return wrap(`<audio src="/static/uploads/${path}${file.name}" controls>`);
else
return '';
});
hbs.registerHelper('licenseLabel', licenseLabel);
hbs.registerHelper('licenseLink', licenseLink);
hbs.registerHelper('fileCredit', function(file, options) {
const label = licenseLabel(file.license, options),
link = licenseLink(file.license, label);
if (!file.creator && !file.uploader)
return i18n.__({
phrase: 'rights in caption, own work',
locale: options.data.root.locale
}, link);
else
return i18n.__({
phrase: 'rights in caption, someone else\'s work',
locale: options.data.root.locale
},
file.creator ?
mlString.resolve(options.data.root.locale, file.creator).str :
userLink(file.uploader),
link);
});
hbs.registerHelper('linkify', str => linkifyHTML(str, {
defaultProtocol: 'https',
target: {
url: '_blank'
// email links won't get target="_blank"
}
}));
function userLink(user) {
return user ? `<a href="/user/${user.urlName}">${user.displayName}</a>` : '';
}
function licenseLabel(license, options) {
const key = license === 'fair-use' ? 'fair use short' : `${license} short`;
return i18n.__({ phrase: key, locale: options.data.root.locale });
}
function licenseLink(license, licenseLabel) {
const url = getLicenseURL(license);
return url ? `<a href="${url}">${licenseLabel}</a>` : licenseLabel;
}