Ghost/core/frontend/helpers/match.js
Hannah Wolfe ba587ba882
Fixed logic bugs in match helper (#13315)
closes: CORE-33

Two bugs:
- lodash isEmpty and handlebars util isEmpty are not the same
- I literally had the truthy and falsy cases the wrong way around 🙈

Notes:
- I have, for now, copied the isEmpty util from handlebars. It's so small it doesn't seem worth trying to require the util right now, although in future it'd be nice if that was easier to do
- Adding the management for the conditional being a SafeString allows the match helper to be a subexpression of itself, I can see this pattern being useful later in combo with the any and all helpers
2021-09-17 09:47:10 +01:00

101 lines
2.8 KiB
JavaScript

const {logging, i18n, SafeString, labs} = require('../services/proxy');
const _ = require('lodash');
/**
* This is identical to the built-in if helper, except inverse/fn calls are replaced with false/true
* https://github.com/handlebars-lang/handlebars.js/blob/19bdace85a8d0bc5ed3a4dec4071cb08c8d003f2/lib/handlebars/helpers/if.js#L9-L20
*/
function isEmptyValue(value) {
if (!value && value !== 0) {
return true;
} else if (Array.isArray(value) && value.length === 0) {
return true;
} else {
return false;
}
}
const handleConditional = (conditional, options) => {
if (_.isFunction(conditional)) {
conditional = conditional.call(this);
}
if (conditional instanceof SafeString) {
conditional = conditional.string;
}
// Default behavior is to render the positive path if the value is truthy and not empty.
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
if ((!options.hash.includeZero && !conditional) || isEmptyValue(conditional)) {
return false;
} else {
return true;
}
};
const handleMatch = (data, operator, value) => {
let result;
switch (operator) {
case '!=':
result = data !== value;
break;
default:
result = data === value;
}
return result;
};
function match(...attrs) {
// options = options || {};
// options.hash = options.hash || {};
// options.data = options.data || {};
const options = attrs.pop();
const isBlock = _.has(options, 'fn');
let result;
if (_.isEmpty(attrs)) {
logging.warn(i18n.t('warnings.helpers.has.invalidAttribute'));
return;
}
if (attrs.length === 1) {
// If we only have one attribute, treat it as simple true/false (like the if helper)
result = handleConditional(attrs[0], options);
} else if (attrs.length === 3) {
result = handleMatch(attrs[0], attrs[1], attrs[2], options);
} else {
logging.warn(i18n.t('warnings.helpers.has.invalidAttribute'));
return;
}
// If we're in block mode, return the outcome from the fn/inverse functions
if (isBlock) {
if (result) {
return options.fn(this);
}
return options.inverse(this);
}
// Else return the result as a string
return new SafeString(result);
}
module.exports = function matchLabsWrapper() {
let self = this;
let args = arguments;
return labs.enabledHelper({
flagKey: 'matchHelper',
flagName: 'Match helper',
helperName: 'match',
helpUrl: 'https://ghost.org/docs/themes/'
}, () => {
return match.apply(self, args);
});
};