react-helmet-async
Version:
Thread-safe Helmet for React 16+ and friends
159 lines (139 loc) • 4.99 kB
JavaScript
import React from 'react';
import {
HELMET_ATTRIBUTE,
TAG_NAMES,
REACT_TAG_MAP,
TAG_PROPERTIES,
ATTRIBUTE_NAMES,
} from './constants';
import { flattenArray } from './utils';
const SELF_CLOSING_TAGS = [TAG_NAMES.NOSCRIPT, TAG_NAMES.SCRIPT, TAG_NAMES.STYLE];
const encodeSpecialCharacters = (str, encode = true) => {
if (encode === false) {
return String(str);
}
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const generateElementAttributesAsString = attributes =>
Object.keys(attributes).reduce((str, key) => {
const attr = typeof attributes[key] !== 'undefined' ? `${key}="${attributes[key]}"` : `${key}`;
return str ? `${str} ${attr}` : attr;
}, '');
const generateTitleAsString = (type, title, attributes, encode) => {
const attributeString = generateElementAttributesAsString(attributes);
const flattenedTitle = flattenArray(title);
return attributeString
? `<${type} ${HELMET_ATTRIBUTE}="true" ${attributeString}>${encodeSpecialCharacters(
flattenedTitle,
encode
)}</${type}>`
: `<${type} ${HELMET_ATTRIBUTE}="true">${encodeSpecialCharacters(
flattenedTitle,
encode
)}</${type}>`;
};
const generateTagsAsString = (type, tags, encode) =>
tags.reduce((str, tag) => {
const attributeHtml = Object.keys(tag)
.filter(
attribute =>
!(attribute === TAG_PROPERTIES.INNER_HTML || attribute === TAG_PROPERTIES.CSS_TEXT)
)
.reduce((string, attribute) => {
const attr =
typeof tag[attribute] === 'undefined'
? attribute
: `${attribute}="${encodeSpecialCharacters(tag[attribute], encode)}"`;
return string ? `${string} ${attr}` : attr;
}, '');
const tagContent = tag.innerHTML || tag.cssText || '';
const isSelfClosing = SELF_CLOSING_TAGS.indexOf(type) === -1;
return `${str}<${type} ${HELMET_ATTRIBUTE}="true" ${attributeHtml}${
isSelfClosing ? `/>` : `>${tagContent}</${type}>`
}`;
}, '');
const convertElementAttributesToReactProps = (attributes, initProps = {}) =>
Object.keys(attributes).reduce((obj, key) => {
// eslint-disable-next-line no-param-reassign
obj[REACT_TAG_MAP[key] || key] = attributes[key];
return obj;
}, initProps);
const generateTitleAsReactComponent = (type, title, attributes) => {
// assigning into an array to define toString function on it
const initProps = {
key: title,
[HELMET_ATTRIBUTE]: true,
};
const props = convertElementAttributesToReactProps(attributes, initProps);
return [React.createElement(TAG_NAMES.TITLE, props, title)];
};
const generateTagsAsReactComponent = (type, tags) =>
tags.map((tag, i) => {
const mappedTag = {
key: i,
[HELMET_ATTRIBUTE]: true,
};
Object.keys(tag).forEach(attribute => {
const mappedAttribute = REACT_TAG_MAP[attribute] || attribute;
if (
mappedAttribute === TAG_PROPERTIES.INNER_HTML ||
mappedAttribute === TAG_PROPERTIES.CSS_TEXT
) {
const content = tag.innerHTML || tag.cssText;
mappedTag.dangerouslySetInnerHTML = { __html: content };
} else {
mappedTag[mappedAttribute] = tag[attribute];
}
});
return React.createElement(type, mappedTag);
});
const getMethodsForTag = (type, tags, encode) => {
switch (type) {
case TAG_NAMES.TITLE:
return {
toComponent: () =>
generateTitleAsReactComponent(type, tags.title, tags.titleAttributes, encode),
toString: () => generateTitleAsString(type, tags.title, tags.titleAttributes, encode),
};
case ATTRIBUTE_NAMES.BODY:
case ATTRIBUTE_NAMES.HTML:
return {
toComponent: () => convertElementAttributesToReactProps(tags),
toString: () => generateElementAttributesAsString(tags),
};
default:
return {
toComponent: () => generateTagsAsReactComponent(type, tags),
toString: () => generateTagsAsString(type, tags, encode),
};
}
};
const mapStateOnServer = ({
baseTag,
bodyAttributes,
encode,
htmlAttributes,
linkTags,
metaTags,
noscriptTags,
scriptTags,
styleTags,
title = '',
titleAttributes,
}) => ({
base: getMethodsForTag(TAG_NAMES.BASE, baseTag, encode),
bodyAttributes: getMethodsForTag(ATTRIBUTE_NAMES.BODY, bodyAttributes, encode),
htmlAttributes: getMethodsForTag(ATTRIBUTE_NAMES.HTML, htmlAttributes, encode),
link: getMethodsForTag(TAG_NAMES.LINK, linkTags, encode),
meta: getMethodsForTag(TAG_NAMES.META, metaTags, encode),
noscript: getMethodsForTag(TAG_NAMES.NOSCRIPT, noscriptTags, encode),
script: getMethodsForTag(TAG_NAMES.SCRIPT, scriptTags, encode),
style: getMethodsForTag(TAG_NAMES.STYLE, styleTags, encode),
title: getMethodsForTag(TAG_NAMES.TITLE, { title, titleAttributes }, encode),
});
export default mapStateOnServer;