UNPKG

react-helmet-async

Version:

Thread-safe Helmet for React 16+ and friends

211 lines (180 loc) 7.01 kB
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 };