react-helmet-async
Version:
Thread-safe Helmet for React 16+ and friends
211 lines (180 loc) • 7.01 kB
JavaScript
import { TAG_NAMES, TAG_PROPERTIES, ATTRIBUTE_NAMES } from './constants';
const HELMET_PROPS = {
DEFAULT_TITLE: 'defaultTitle',
DEFER: 'defer',
ENCODE_SPECIAL_CHARACTERS: 'encodeSpecialCharacters',
ON_CHANGE_CLIENT_STATE: 'onChangeClientState',
TITLE_TEMPLATE: 'titleTemplate',
};
const getInnermostProperty = (propsList, property) => {
for (let i = propsList.length - 1; i >= 0; i -= 1) {
const props = propsList[i];
if (Object.prototype.hasOwnProperty.call(props, property)) {
return props[property];
}
}
return null;
};
const getTitleFromPropsList = propsList => {
let innermostTitle = getInnermostProperty(propsList, TAG_NAMES.TITLE);
const innermostTemplate = getInnermostProperty(propsList, HELMET_PROPS.TITLE_TEMPLATE);
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(propsList, HELMET_PROPS.DEFAULT_TITLE);
return innermostTitle || innermostDefaultTitle || undefined;
};
const getOnChangeClientState = propsList =>
getInnermostProperty(propsList, HELMET_PROPS.ON_CHANGE_CLIENT_STATE) || (() => {});
const getAttributesFromPropsList = (tagType, propsList) =>
propsList
.filter(props => typeof props[tagType] !== 'undefined')
.map(props => props[tagType])
.reduce((tagAttrs, current) => ({ ...tagAttrs, ...current }), {});
const getBaseTagFromPropsList = (primaryAttributes, propsList) =>
propsList
.filter(props => typeof props[TAG_NAMES.BASE] !== 'undefined')
.map(props => props[TAG_NAMES.BASE])
.reverse()
.reduce((innermostBaseTag, tag) => {
if (!innermostBaseTag.length) {
const keys = Object.keys(tag);
for (let i = 0; i < keys.length; i += 1) {
const attributeKey = keys[i];
const lowerCaseAttributeKey = attributeKey.toLowerCase();
if (
primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 &&
tag[lowerCaseAttributeKey]
) {
return innermostBaseTag.concat(tag);
}
}
}
return innermostBaseTag;
}, []);
// eslint-disable-next-line no-console
const warn = msg => console && typeof console.warn === 'function' && console.warn(msg);
const getTagsFromPropsList = (tagName, primaryAttributes, propsList) => {
// Calculate list of tags, giving priority innermost component (end of the propslist)
const approvedSeenTags = {};
return propsList
.filter(props => {
if (Array.isArray(props[tagName])) {
return true;
}
if (typeof props[tagName] !== 'undefined') {
warn(
`Helmet: ${tagName} should be of type "Array". Instead found type "${typeof props[
tagName
]}"`
);
}
return false;
})
.map(props => props[tagName])
.reverse()
.reduce((approvedTags, instanceTags) => {
const instanceSeenTags = {};
instanceTags
.filter(tag => {
let primaryAttributeKey;
const keys = Object.keys(tag);
for (let i = 0; i < keys.length; i += 1) {
const attributeKey = keys[i];
const lowerCaseAttributeKey = attributeKey.toLowerCase();
// Special rule with link tags, since rel and href are both primary tags, rel takes priority
if (
primaryAttributes.indexOf(lowerCaseAttributeKey) !== -1 &&
!(
primaryAttributeKey === TAG_PROPERTIES.REL &&
tag[primaryAttributeKey].toLowerCase() === 'canonical'
) &&
!(
lowerCaseAttributeKey === TAG_PROPERTIES.REL &&
tag[lowerCaseAttributeKey].toLowerCase() === 'stylesheet'
)
) {
primaryAttributeKey = lowerCaseAttributeKey;
}
// Special case for innerHTML which doesn't work lowercased
if (
primaryAttributes.indexOf(attributeKey) !== -1 &&
(attributeKey === TAG_PROPERTIES.INNER_HTML ||
attributeKey === TAG_PROPERTIES.CSS_TEXT ||
attributeKey === TAG_PROPERTIES.ITEM_PROP)
) {
primaryAttributeKey = attributeKey;
}
}
if (!primaryAttributeKey || !tag[primaryAttributeKey]) {
return false;
}
const value = tag[primaryAttributeKey].toLowerCase();
if (!approvedSeenTags[primaryAttributeKey]) {
approvedSeenTags[primaryAttributeKey] = {};
}
if (!instanceSeenTags[primaryAttributeKey]) {
instanceSeenTags[primaryAttributeKey] = {};
}
if (!approvedSeenTags[primaryAttributeKey][value]) {
instanceSeenTags[primaryAttributeKey][value] = true;
return true;
}
return false;
})
.reverse()
.forEach(tag => approvedTags.push(tag));
// Update seen tags with tags from this instance
const keys = Object.keys(instanceSeenTags);
for (let i = 0; i < keys.length; i += 1) {
const attributeKey = keys[i];
const tagUnion = {
...approvedSeenTags[attributeKey],
...instanceSeenTags[attributeKey],
};
approvedSeenTags[attributeKey] = tagUnion;
}
return approvedTags;
}, [])
.reverse();
};
const reducePropsToState = propsList => ({
baseTag: getBaseTagFromPropsList([TAG_PROPERTIES.HREF], propsList),
bodyAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.BODY, propsList),
defer: getInnermostProperty(propsList, HELMET_PROPS.DEFER),
encode: getInnermostProperty(propsList, HELMET_PROPS.ENCODE_SPECIAL_CHARACTERS),
htmlAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.HTML, propsList),
linkTags: getTagsFromPropsList(
TAG_NAMES.LINK,
[TAG_PROPERTIES.REL, TAG_PROPERTIES.HREF],
propsList
),
metaTags: getTagsFromPropsList(
TAG_NAMES.META,
[
TAG_PROPERTIES.NAME,
TAG_PROPERTIES.CHARSET,
TAG_PROPERTIES.HTTPEQUIV,
TAG_PROPERTIES.PROPERTY,
TAG_PROPERTIES.ITEM_PROP,
],
propsList
),
noscriptTags: getTagsFromPropsList(TAG_NAMES.NOSCRIPT, [TAG_PROPERTIES.INNER_HTML], propsList),
onChangeClientState: getOnChangeClientState(propsList),
scriptTags: getTagsFromPropsList(
TAG_NAMES.SCRIPT,
[TAG_PROPERTIES.SRC, TAG_PROPERTIES.INNER_HTML],
propsList
),
styleTags: getTagsFromPropsList(TAG_NAMES.STYLE, [TAG_PROPERTIES.CSS_TEXT], propsList),
title: getTitleFromPropsList(propsList),
titleAttributes: getAttributesFromPropsList(ATTRIBUTE_NAMES.TITLE, propsList),
});
export const flattenArray = possibleArray =>
Array.isArray(possibleArray) ? possibleArray.join('') : possibleArray;
export { reducePropsToState };