import React from "react"; import withSideEffect from "./side-effect"; import {AmpModeContext} from './amphtml-context'; import { HeadManagerContext } from "./head-manager-context"; import { isAmp } from './amp' type WithIsAmp = { isAmp?: boolean; } export function defaultHead(className = 'next-head', isAmp = false) { const head = [ , ]; if (!isAmp) { head.push( , ); } return head; } function onlyReactElement( list: Array>, child: React.ReactChild, ): Array> { // React children can be "string" or "number" in this case we ignore them for backwards compat if (typeof child === "string" || typeof child === "number") { return list; } // Adds support for React.Fragment if (child.type === React.Fragment) { return list.concat( React.Children.toArray(child.props.children).reduce(( fragmentList: Array>, fragmentChild: React.ReactChild, ): Array> => { if ( typeof fragmentChild === "string" || typeof fragmentChild === "number" ) { return fragmentList; } return fragmentList.concat(fragmentChild); }, []), ); } return list.concat(child); } const METATYPES = ["name", "httpEquiv", "charSet", "viewport", "itemProp"]; /* returns a function for filtering head child elements which shouldn't be duplicated, like Also adds support for deduplicated `key` properties */ function unique() { const keys = new Set(); const tags = new Set(); const metaTypes = new Set(); const metaCategories: { [metatype: string]: Set<string> } = {}; return (h: React.ReactElement<any>) => { if (h.key && typeof h.key !== 'number' && h.key.indexOf(".$") === 0) { if (keys.has(h.key)) return false; keys.add(h.key); return true; } switch (h.type) { case "title": case "base": if (tags.has(h.type)) return false; tags.add(h.type); break; case "meta": for (let i = 0, len = METATYPES.length; i < len; i++) { const metatype = METATYPES[i]; if (!h.props.hasOwnProperty(metatype)) continue; if (metatype === "charSet" || metatype === "viewport") { if (metaTypes.has(metatype)) return false; metaTypes.add(metatype); } else { const category = h.props[metatype]; const categories = metaCategories[metatype] || new Set(); if (categories.has(category)) return false; categories.add(category); metaCategories[metatype] = categories; } } break; } return true; }; } /** * * @param headElement List of multiple <Head> instances */ function reduceComponents(headElements: Array<React.ReactElement<any>>, props: WithIsAmp) { return headElements .reduce( (list: React.ReactChild[], headElement: React.ReactElement<any>) => { const headElementChildren = React.Children.toArray( headElement.props.children, ); return list.concat(headElementChildren); }, [], ) .reduce(onlyReactElement, []) .reverse() .concat(defaultHead('', props.isAmp)) .filter(unique()) .reverse() .map((c: React.ReactElement<any>, i: number) => { const className = (c.props && c.props.className ? c.props.className + " " : "") + "next-head"; const key = c.key || i; return React.cloneElement(c, { key, className }); }); } const Effect = withSideEffect(); function Head({ children }: { children: React.ReactNode }) { return ( <AmpModeContext.Consumer> {(ampMode) => ( <HeadManagerContext.Consumer> {(updateHead) => ( <Effect reduceComponentsToState={reduceComponents} handleStateChange={updateHead} isAmp={isAmp(ampMode)} > {children} </Effect> )} </HeadManagerContext.Consumer> )} </AmpModeContext.Consumer> ); } Head.rewind = Effect.rewind; export default Head