/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/** @module view-helper */
import {marked} from 'marked';
import DOMPurify from 'dompurify';
import Handlebars from 'handlebars';
/**
* A view helper.
*/
class ViewHelper {
constructor() {
this._registerHandlebarsHelpers();
/** @private */
this.mdBeforeList = [
/*{
regex: /```\n?([\s\S]*?)```/g,
value: (s, string) => {
return '```\n' + string.replace(/\\\>/g, '>') + '```';
},
},*/
{
// Also covers triple-backtick blocks.
regex: /`([\s\S]*?)`/g,
value: (s, string) => {
// noinspection RegExpRedundantEscape
return '`' + string.replace(/\\\ 99) {
node.removeAttribute('start');
}
if (node instanceof HTMLFormElement) {
if (node.action) {
node.removeAttribute('action');
}
if (node.hasAttribute('method')) {
node.removeAttribute('method');
}
}
if (node instanceof HTMLButtonElement) {
if (node.type === 'submit') {
node.type = 'button';
}
}
});
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if (node instanceof HTMLAnchorElement) {
const href = node.getAttribute('href');
if (href && !href.startsWith('#')) {
node.setAttribute('rel', 'noopener noreferrer');
}
if (node.targetBlank) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
}
});
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
if (data.attrName === 'style') {
const style = data.attrValue
.split(';')
.map(s => s.trim())
.filter(rule => {
const [property, value] = rule.split(':')
.map(s => s.trim().toLowerCase());
if (
property === 'position' &&
['absolute', 'fixed', 'sticky'].includes(value)
) {
return false;
}
return true;
});
data.attrValue = style.join('; ');
}
});
}
/**
* A layout manager.
*
* @type {module:layout-manager}
*/
layoutManager = null
/**
* A config.
*
* @type {module:models/settings}
*/
settings = null
/**
* A config.
*
* @type {module:models/settings}
*/
config = null
/**
* A current user.
*
* @type {module:models/user}
*/
user = null
/**
* A preferences.
*
* @type {module:models/preferences}
*/
preferences = null
/**
* An ACL manager.
*
* @type {module:acl-manager}
*/
acl = null
/**
* A model factory.
*
* @type {module:model-factory}
*/
modelFactory = null
/**
* A collection factory.
*
* @type {module:collection-factory}
*/
collectionFactory = null
/**
* A router.
*
* @type {module:router}
*/
router = null
/**
* A storage.
*
* @type {module:storage}
*/
storage = null
/**
* A session storage.
*
* @type {module:session-storage}
*/
sessionStorage = null
/**
* A date-time util.
*
* @type {module:date-time}
*/
dateTime = null
/**
* A language.
*
* @type {module:language}
*/
language = null
/**
* A metadata.
*
* @type {module:metadata}
*/
metadata = null
/**
* A field-manager util.
*
* @type {module:field-manager}
*/
fieldManager = null
/**
* A cache.
*
* @type {module:cache}
*/
cache = null
/**
* A theme manager.
*
* @type {module:theme-manager}
*/
themeManager = null
/**
* A web-socket manager. Null if not enabled.
*
* @type {?module:web-socket-manager}
*/
webSocketManager = null
/**
* A number util.
*
* @type {module:num-util}
*/
numberUtil = null
/**
* A page-title util.
*
* @type {module:page-title}
*/
pageTitle = null
/**
* A broadcast channel.
*
* @type {?module:broadcast-channel}
*/
broadcastChannel = null
/**
* A base path.
*
* @type {string}
*/
basePath = ''
/**
* Application parameters.
*
* @type {import('app-params').default|null}
*/
appParams = null
/**
* @private
*/
_registerHandlebarsHelpers() {
Handlebars.registerHelper('img', img => {
return new Handlebars.SafeString(`
`);
});
Handlebars.registerHelper('prop', (object, name) => {
if (object === undefined) {
console.warn(`Undefined value passed to 'prop' helper.`);
return undefined;
}
if (name in object) {
return object[name];
}
return undefined;
});
Handlebars.registerHelper('var', (name, context, options) => {
if (typeof context === 'undefined') {
return null;
}
let contents = context[name];
if (options.hash.trim) {
contents = contents.trim();
}
return new Handlebars.SafeString(contents);
});
Handlebars.registerHelper('concat', function (left, right) {
return left + right;
});
Handlebars.registerHelper('ifEqual', function (left, right, options) {
// noinspection EqualityComparisonWithCoercionJS
if (left == right) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifNotEqual', function (left, right, options) {
// noinspection EqualityComparisonWithCoercionJS
if (left != right) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifPropEquals', function (object, property, value, options) {
// noinspection EqualityComparisonWithCoercionJS
if (object[property] == value) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifAttrEquals', function (model, attr, value, options) {
// noinspection EqualityComparisonWithCoercionJS
if (model.get(attr) == value) {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('ifAttrNotEmpty', function (model, attr, options) {
const value = model.get(attr);
if (value !== null && typeof value !== 'undefined') {
return options.fn(this);
}
return options.inverse(this);
});
Handlebars.registerHelper('get', (model, name) => model.get(name));
Handlebars.registerHelper('length', arr => arr.length);
Handlebars.registerHelper('translate', (name, options) => {
const scope = options.hash.scope || null;
const category = options.hash.category || null;
if (name === 'null') {
return '';
}
return this.language.translate(name, category, scope);
});
Handlebars.registerHelper('dropdownItem', (name, options) => {
const scope = options.hash.scope || null;
const label = options.hash.label;
const labelTranslation = options.hash.labelTranslation;
const data = options.hash.data;
const hidden = options.hash.hidden;
const disabled = options.hash.disabled;
const title = options.hash.title;
const link = options.hash.link;
const action = options.hash.action || name;
const iconHtml = options.hash.iconHtml;
const iconClass = options.hash.iconClass;
let html =
options.hash.html ||
options.hash.text ||
(
labelTranslation ?
this.language.translatePath(labelTranslation) :
this.language.translate(label, 'labels', scope)
);
if (!options.hash.html) {
html = this.escapeString(html);
}
if (iconHtml) {
html = iconHtml + ' ' + html;
}
else if (iconClass) {
const iconHtml = $('').addClass(iconClass).get(0).outerHTML;
html = iconHtml + ' ' + html;
}
const $li = $('')
.addClass(hidden ? 'hidden' : '')
.addClass(disabled ? 'disabled' : '');
const $a = $('')
.attr('role', 'button')
.attr('tabindex', '0')
.attr('data-name', name)
.addClass(options.hash.className || '')
.addClass('action')
.html(html);
if (action) {
$a.attr('data-action', action);
}
$li.append($a);
link ?
$a.attr('href', link) :
$a.attr('role', 'button');
if (data) {
for (const key in data) {
$a.attr('data-' + Espo.Utils.camelCaseToHyphen(key), data[key]);
}
}
if (disabled) {
$li.attr('disabled', 'disabled');
}
if (title) {
$a.attr('title', title);
}
return new Handlebars.SafeString($li.get(0).outerHTML);
});
Handlebars.registerHelper('button', (name, options) => {
const style = options.hash.style || 'default';
const scope = options.hash.scope || null;
const label = options.hash.label || name;
const labelTranslation = options.hash.labelTranslation;
const link = options.hash.link;
const iconHtml = options.hash.iconHtml;
const iconClass = options.hash.iconClass;
let html =
options.hash.html ||
options.hash.text ||
(
labelTranslation ?
this.language.translatePath(labelTranslation) :
this.language.translate(label, 'labels', scope)
);
if (!options.hash.html) {
html = this.escapeString(html);
}
if (iconHtml) {
html = iconHtml + ' ' + '' + html + '';
}
else if (iconClass) {
const iconHtml = $('').addClass(iconClass).get(0).outerHTML;
html = iconHtml + ' ' + '' + html + '';
}
const tag = link ? '' : '