@dr.pogodin/react-helmet
Version:
Thread-safe Helmet for React 19+ and friends
289 lines (270 loc) • 10.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.aggregateBaseProps = aggregateBaseProps;
exports.calcAggregatedState = calcAggregatedState;
exports.cloneProps = cloneProps;
exports.flattenArray = flattenArray;
exports.getTagsFromPropsList = getTagsFromPropsList;
exports.getTitleFromPropsList = getTitleFromPropsList;
exports.mergeAttributes = mergeAttributes;
exports.mergeProps = mergeProps;
exports.prioritizer = prioritizer;
exports.propToAttr = propToAttr;
exports.pushToPropArray = pushToPropArray;
exports.without = void 0;
var _constants = require("./constants");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
/**
* Finds the last object in the given array of registered props,
* that has the specified prop defined, and returns the value of
* that prop in that object. Returns `undefined` if no prop object
* has that prop defined.
*/
function getInnermostProperty(props, propName) {
for (let i = props.length - 1; i >= 0; --i) {
const value = props[i][1][propName];
if (value !== undefined) return value;
}
return undefined;
}
function getTitleFromPropsList(props) {
let innermostTitle = getInnermostProperty(props, _constants.TAG_NAMES.TITLE);
const innermostTemplate = getInnermostProperty(props, 'titleTemplate');
if (Array.isArray(innermostTitle)) {
innermostTitle = innermostTitle.join('');
}
if (innermostTemplate && innermostTitle) {
// use function arg to avoid need to escape $ characters
return innermostTemplate.replace(/%s/g, () => innermostTitle);
}
const innermostDefaultTitle = getInnermostProperty(props, 'defaultTitle');
// NOTE: We really want || here to match legacy behavior, where default title
// was applied also when the given title was an empty string.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return (innermostTitle || innermostDefaultTitle) ?? undefined;
}
/**
* Merges together attributes provided for the same element by different Helmet
* instances. Attributes provided by later registered Helmet instances overwrite
* the same attributes provided by the earlier registered instances.
*/
function mergeAttributes(element, props) {
const res = {};
for (const item of props) {
const attrs = item[1][element];
if (attrs) Object.assign(res, attrs);
}
return res;
}
/**
* Finds the latest registered Helmet instance with `base` props provided,
* and with its `href` value set, and returns those `base` props.
* NOTE: Based on the legacy getBaseTagFromPropsList().
*/
function aggregateBaseProps(props) {
for (let i = props.length - 1; i >= 0; --i) {
const res = props[i][1].base;
if (res?.href) return res;
}
return undefined;
}
/**
* Determines the primary key in the given `props` object, accoding to the given
* array of valid primary keys for the kind of props object.
* TODO: Rather than passing an array of primaryProps around, it might be more
* simple to just have a dedicated function for each possible kind of that
* object.
*/
function getPrimaryProp(props, primaryProps) {
// Looks up for the "primary attribute" key.
let primaryAttributeKey;
// TODO: Perhaps also check that the value of attribute being selected
// as primary is actually defined? Right now, it implicitly assumes that
// in such case the attribute is just not present as a key in `props`.
for (const keyString of Object.keys(props)) {
const key = keyString;
// Special rule with link tags, since rel and href are both primary tags,
// rel takes priority
if (primaryProps.includes(key) && !(primaryAttributeKey === _constants.TAG_PROPERTIES.REL && props[primaryAttributeKey].toLowerCase() === 'canonical') && !(key === _constants.TAG_PROPERTIES.REL && props[key].toLowerCase() === 'stylesheet')) primaryAttributeKey = key;
// Special case for innerHTML which doesn't work lowercased
if (primaryProps.includes(key) && (key === _constants.TAG_PROPERTIES.INNER_HTML || key === _constants.TAG_PROPERTIES.CSS_TEXT || key === _constants.TAG_PROPERTIES.ITEM_PROP)) primaryAttributeKey = key;
}
return primaryAttributeKey ?? null;
}
function getTagsFromPropsList(tagName, primaryAttributes, propsArray) {
// Calculate list of tags, giving priority innermost component
// (end of the propslist)
const approvedSeenTags = {};
// TODO: Well, this is a touch one to refactor, while ensuring it does not
// change any behavior aspect... let's stick to the legacy implementation,
// with minimal updates, for now, then refactor it later.
return propsArray.map(_ref => {
let [, props] = _ref;
return props;
}).filter(props => {
if (Array.isArray(props[tagName])) {
return true;
}
if (typeof props[tagName] !== 'undefined') {
// eslint-disable-next-line no-console
console.warn(`Helmet: ${tagName} should be of type "Array". Instead found type "${typeof props[tagName]}"`);
}
return false;
}).map(props => props[tagName]).reverse()
// From last to first.
.reduce((approvedTags, instanceTags) => {
const instanceSeenTags = {};
instanceTags.filter(tag => {
const primaryAttributeKey = getPrimaryProp(tag, primaryAttributes);
if (!primaryAttributeKey || !tag[primaryAttributeKey]) {
return false;
}
const value = tag[primaryAttributeKey].toLowerCase();
if (!approvedSeenTags[primaryAttributeKey]) {
approvedSeenTags[primaryAttributeKey] = {};
}
if (!instanceSeenTags[primaryAttributeKey]) {
instanceSeenTags[primaryAttributeKey] = {};
}
// essentially we collect every item that haven't been seen so far?
if (!approvedSeenTags[primaryAttributeKey][value]) {
instanceSeenTags[primaryAttributeKey][value] = true;
return true;
}
return false;
}).reverse()
// so approved tags are accumulated from last to first
.forEach(tag => approvedTags.push(tag));
// Update seen tags with tags from this instance
const keys = Object.keys(instanceSeenTags);
for (const attributeKey of keys) {
const tagUnion = {
...approvedSeenTags[attributeKey],
...instanceSeenTags[attributeKey]
};
approvedSeenTags[attributeKey] = tagUnion;
}
return approvedTags;
}, [])
// then reversed back to the from first-to-last order.
.reverse();
}
function getAnyTrueFromPropsArray(propsArray, propName) {
for (const [, props] of propsArray) {
if (props[propName]) return true;
}
return false;
}
function flattenArray(possibleArray) {
return Array.isArray(possibleArray) ? possibleArray.join('') : possibleArray;
}
function checkIfPropsMatch(props, toMatch) {
for (const key of Object.keys(props)) {
// e.g. if rel exists in the list of allowed props [amphtml, alternate, etc]
// TODO: Do a better typing job here.
if (toMatch[key]?.includes(props[key])) return true;
}
return false;
}
function prioritizer(propsArray, propsToMatch) {
const res = {
default: Array(),
priority: Array()
};
if (propsArray) {
for (const props of propsArray) {
if (checkIfPropsMatch(props, propsToMatch)) {
res.priority.push(props);
} else {
res.default.push(props);
}
}
}
return res;
}
// TODO: Perhaps, we better destruct the `obj` first, and then create the result
// not including the key altogether?
const without = (obj, key) => ({
...obj,
[key]: undefined
});
exports.without = without;
/**
* Clones given props object deep enough to make it safe to push new items
* to its array values, and re-assign its non-array values, without a risk
* to mutate any externally owned objects.
*/
function cloneProps(props) {
const res = {};
for (const [key, value] of Object.entries(props)) {
res[key] = Array.isArray(value) ? value.slice() : value;
}
return res;
}
/**
* Merges `source` props into `target`, mutating the `target` object.
*/
function mergeProps(target, source) {
const tgt = target;
for (const [key, srcValue] of Object.entries(source)) {
if (Array.isArray(srcValue)) {
const tgtValue = tgt[key];
tgt[key] = tgtValue ? tgtValue.concat(srcValue) : srcValue;
} else tgt[key] = srcValue;
}
}
/**
* Adds given item to the specified prop array inside `target`.
* It mutates the target.
*/
function pushToPropArray(target, array, item) {
const tgt = target[array];
if (tgt) tgt.push(item);else target[array] = [item];
}
function calcAggregatedState(props) {
let links = getTagsFromPropsList(_constants.TAG_NAMES.LINK, [_constants.TAG_PROPERTIES.REL, _constants.TAG_PROPERTIES.HREF], props);
let meta = getTagsFromPropsList('meta', [
// NOTE: In the legacy version "charSet", "httpEquiv", and "itemProp"
// were given as HTML attributes: charset, http-equiv, itemprop.
// I believe, it is already fine to replace them here now, but
// let's be vigilant.
_constants.TAG_PROPERTIES.NAME, 'charSet', 'httpEquiv', _constants.TAG_PROPERTIES.PROPERTY, 'itemProp'], props);
let script = getTagsFromPropsList('script', [_constants.TAG_PROPERTIES.SRC, _constants.TAG_PROPERTIES.INNER_HTML], props);
const prioritizeSeoTags = getAnyTrueFromPropsArray(props, 'prioritizeSeoTags');
let priority;
if (prioritizeSeoTags) {
const linkP = prioritizer(links, _constants.SEO_PRIORITY_TAGS.link);
links = linkP.default;
const metaP = prioritizer(meta, _constants.SEO_PRIORITY_TAGS.meta);
meta = metaP.default;
const scriptP = prioritizer(script, _constants.SEO_PRIORITY_TAGS.script);
script = scriptP.default;
priority = {
links: linkP.priority,
meta: metaP.priority,
script: scriptP.priority
};
}
return {
base: aggregateBaseProps(props),
bodyAttributes: mergeAttributes('bodyAttributes', props),
defer: getInnermostProperty(props, 'defer'),
encodeSpecialCharacters: getInnermostProperty(props, 'encodeSpecialCharacters') ?? true,
htmlAttributes: mergeAttributes('htmlAttributes', props),
links,
meta,
noscript: getTagsFromPropsList('noscript', [_constants.TAG_PROPERTIES.INNER_HTML], props),
onChangeClientState: getInnermostProperty(props, 'onChangeClientState'),
priority,
script,
style: getTagsFromPropsList('style', [_constants.TAG_PROPERTIES.CSS_TEXT], props),
title: getTitleFromPropsList(props),
titleAttributes: mergeAttributes('titleAttributes', props)
};
}
function propToAttr(prop) {
return _constants.HTML_TAG_MAP[prop] ?? prop;
}
//# sourceMappingURL=utils.js.map