@lomray/react-head-manager 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/manager.d.ts +23 -1
- package/manager.js +1 -1
- package/manager.js.map +1 -1
- package/package.json +5 -2
- package/root-attributes.d.ts +14 -0
- package/root-attributes.js +2 -0
- package/root-attributes.js.map +1 -0
package/README.md
CHANGED
|
@@ -89,6 +89,10 @@ manager.setTagsDefinitions({
|
|
|
89
89
|
|
|
90
90
|
Explore [demo app](https://github.com/Lomray-Software/vite-template) to more understand.
|
|
91
91
|
|
|
92
|
+
## Bundle size
|
|
93
|
+
|
|
94
|
+
The browser build reads the existing head from the DOM and ships no HTML parser. `html-react-parser` is used only by the server helper.
|
|
95
|
+
|
|
92
96
|
## Bugs and feature requests
|
|
93
97
|
|
|
94
98
|
Bug or a feature request, [please open a new issue](https://github.com/Lomray-Software/react-head-manager/issues/new).
|
package/manager.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ declare class Manager {
|
|
|
55
55
|
* Scheduled synchronization
|
|
56
56
|
*/
|
|
57
57
|
protected syncTimerId: null | NodeJS.Timeout;
|
|
58
|
+
/** DOM ownership is private client state, separate from the public tag snapshot. */
|
|
59
|
+
private rootAttributes;
|
|
58
60
|
/**
|
|
59
61
|
* @constructor
|
|
60
62
|
*/
|
|
@@ -133,7 +135,7 @@ declare class Manager {
|
|
|
133
135
|
/**
|
|
134
136
|
* Clone react element
|
|
135
137
|
*/
|
|
136
|
-
protected cloneElement(element: ReactElement): {
|
|
138
|
+
protected cloneElement(element: Pick<ReactElement, 'type' | 'props'>): {
|
|
137
139
|
element: ReactElement;
|
|
138
140
|
elementProps: Record<string, any>;
|
|
139
141
|
};
|
|
@@ -151,6 +153,16 @@ declare class Manager {
|
|
|
151
153
|
* Push react elements to meta state
|
|
152
154
|
*/
|
|
153
155
|
protected pushElements(elements: ReactNode, containerId: string, isReplace?: boolean, status?: TagStatus): void;
|
|
156
|
+
/**
|
|
157
|
+
* Register a React element or a snapshot of a live DOM element.
|
|
158
|
+
*/
|
|
159
|
+
/**
|
|
160
|
+
* Register a React element or a snapshot of a live DOM element.
|
|
161
|
+
*/
|
|
162
|
+
protected pushElement(child: {
|
|
163
|
+
type: string;
|
|
164
|
+
props: Record<string, any>;
|
|
165
|
+
}, index: number, containerId: string, isReplace: boolean, status: TagStatus, domElement?: HTMLElement): void;
|
|
154
166
|
/**
|
|
155
167
|
* Get html or body props
|
|
156
168
|
*/
|
|
@@ -165,6 +177,9 @@ declare class Manager {
|
|
|
165
177
|
* Apply props to dom element
|
|
166
178
|
*/
|
|
167
179
|
protected applyDomElementAttributes(element: Element, props?: Record<string, any>): void;
|
|
180
|
+
/** Merge root style objects while retaining ownership of each contributed property. */
|
|
181
|
+
/** Merge root style objects while retaining ownership of each contributed property. */
|
|
182
|
+
private syncRootAttributes;
|
|
168
183
|
/**
|
|
169
184
|
* Synchronize local meta tags with head meta tags
|
|
170
185
|
*/
|
|
@@ -186,6 +201,13 @@ declare class Manager {
|
|
|
186
201
|
* Remove meta tags
|
|
187
202
|
*/
|
|
188
203
|
removeTags(containerId: string): void;
|
|
204
|
+
/**
|
|
205
|
+
* Read attributes and text from the live DOM without serializing markup.
|
|
206
|
+
*/
|
|
207
|
+
/**
|
|
208
|
+
* Read attributes and text from the live DOM without serializing markup.
|
|
209
|
+
*/
|
|
210
|
+
protected getDomElementProps(element: HTMLElement): Record<string, any>;
|
|
189
211
|
/**
|
|
190
212
|
* Initial analyze client head meta tags
|
|
191
213
|
*/
|
package/manager.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import e from"@lomray/event-manager";import t
|
|
1
|
+
import e from"@lomray/event-manager";import t,{Fragment as r,Children as s}from"react";import o from"./events.js";import i from"./root-attributes.js";import n from"./tag-status.js";const a=new Map([["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"],["charSet","charset"],["crossOrigin","crossorigin"],["referrerPolicy","referrerpolicy"],["tabIndex","tabindex"],["hrefLang","hreflang"],["imageSrcSet","imagesrcset"],["imageSizes","imagesizes"],["fetchPriority","fetchpriority"],["noModule","nomodule"],["srcSet","srcset"],["acceptCharset","accept-charset"],["itemProp","itemprop"],["itemScope","itemscope"],["itemType","itemtype"],["itemID","itemid"],["itemRef","itemref"]]),l=new Map([...a].map((([e,t])=>[t,e]))),c=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","disablepictureinpicture","disableremoteplayback","formnovalidate","hidden","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","scoped","seamless","selected"]);class d{static rootContainerId="root";isServer="undefined"==typeof window;tags;tagsDefinitions={"meta[charset]":{order:10},"meta[name='viewport']":{order:20},title:{order:100},base:{order:110},meta:{order:200},link:{order:300},script:{order:400},noscript:{order:500},style:{order:600}};reservedAttributes={order:"data-order"};syncTimerId=null;rootAttributes=new WeakMap;constructor(e={}){this.tags={html:new Map(e?.html),body:new Map(e?.body),meta:new Map(e?.meta),containers:new Set(e?.containers)}}setTagsDefinitions(e){this.tagsDefinitions={...this.tagsDefinitions,...e}}static cleanupElementProps(e){const{children:t,dangerouslySetInnerHTML:r,...s}=e;return s}sortTags(e){return new Map([...e.entries()].sort((([,e],[,t])=>e.order-t.order)))}getTags(){const{meta:e,body:t,html:r,containers:s}=this.tags;return{html:this.sortTags(r),body:this.sortTags(t),meta:this.sortTags(e),containers:s}}replaceAttribute(e,t){return a.get(t)??t}createDomElement(e){if(this.isServer)return;const{type:t,props:r}=e,s=document.createElement(t);return this.applyDomElementAttributes(s,r),s}buildKeyByProps(e,t={},r=!1){let s="";for(const e of["id","name","property","href","src"])if(t[e]){s=`[${e}='${t[e]}']`;break}if(s||r||(s=Object.entries(t).map((([e,t])=>"string"==typeof t&&`[${e}]`)).filter(Boolean).join("")),s)return`${e}${s}`}getDefaultKey(e,t,r){return`${e}-${t}-${r}-not-unique`}isNotUniqueTag(e){return e.endsWith("-not-unique")}cloneElement(e){const{type:r}=e,s={...e?.props??{}};return Object.values(this.reservedAttributes).forEach((e=>{s[e]&&delete s[e]})),"title"===r&&Array.isArray(s.children)&&(s.children=s.children.join("")),{element:t.createElement(r,s),elementProps:s}}getElementOrder(e,t,r="unknown"){return(e[this.reservedAttributes.order]?Number(e[this.reservedAttributes.order]):void 0)??this.tagsDefinitions[r]?.order??this.tagsDefinitions[t]?.order??1e3}pushElements(e,t,o=!0,i=n.init){const a=e&&"object"==typeof e&&"type"in e&&e.type===r?e.props.children:e;s.forEach(a,((e,r)=>{e&&"object"==typeof e&&"type"in e&&e.type&&"string"==typeof e.type&&this.pushElement({type:e.type,props:e.props},r,t,o,i)})),this.tags.containers.add(t)}pushElement(e,t,r,s,o,i){const{type:a}=e,{element:l,elementProps:c}=this.cloneElement(e);let m=this.tagsDefinitions[a]?.key??a;switch(a){case"title":break;case"meta":const{charSet:e,httpEquiv:o}=c;m=e?"meta[charset]":o?"meta[httpEquiv]":this.buildKeyByProps(a,c);break;case"html":case"body":if(!s&&this.tags[a].has(m))return;return void this.tags[a].set(r,{props:d.cleanupElementProps(c),order:r===d.rootContainerId?1:this.getElementOrder(c,a,m)});default:m=this.buildKeyByProps(a,c,!0)??this.getDefaultKey(a,r,t)}const u=this.tags.meta.get(m);s||!u?this.isNotUniqueTag(m)&&u?.status===n.synced||this.tags.meta.set(m,{element:this.isServer?l:void 0,domElement:i??(r===d.rootContainerId?void 0:this.createDomElement(l)),order:this.getElementOrder(c,a,m),containerId:r,status:o}):i&&u.domElement?.parentNode!==i.parentNode&&(u.domElement=i)}getRootTagProps(e){return e.size?[...e.values()].reduce(((e,t)=>({...e,...t.props})),{}):{}}applyDomElementAttributes(e,t={}){const r=e.tagName.toLowerCase(),s=Object.values(this.reservedAttributes);Object.entries(t).forEach((([t,o])=>{if(s.includes(t))return;if("children"===t)return void(e.innerHTML=!1===o||null==o?"":o);const i=this.replaceAttribute(r,t);if(!1!==o&&null!=o)return"style"===t&&"object"==typeof o?Object.entries(o).forEach((([t,r])=>{e.style[t]=r})):void e.setAttribute(i,!0===o?"":o);e.removeAttribute(i)}))}syncRootAttributes(e,t,r=!1){const s=this.getRootTagProps(t),o=new Map(Object.entries(s).filter((([e])=>!Object.values(this.reservedAttributes).includes(e))).map((([t,r])=>[this.replaceAttribute(e.tagName.toLowerCase(),t),!1===r||null==r?null:!0===r?"":String(r)]))),n=[...t.values()].reduce(((e,{props:t})=>"style"in t?t.style&&"object"==typeof t.style?{...e,...t.style}:{}:e),{});let a=this.rootAttributes.get(e);a||(a=new i,this.rootAttributes.set(e,a)),a.sync(e,o,n,r)}syncMeta(t){const{meta:r,html:s,body:i}=this.getTags();this.syncTimerId&&clearTimeout(this.syncTimerId);for(const{name:e,value:t}of[{name:"html",value:s},{name:"body",value:i}]){const r=document.querySelector(e);r&&this.syncRootAttributes(r,t)}if(!r.size||this.isServer)return;const a=document.getElementsByTagName("head")?.[0];let l;r.forEach(((e,s)=>{const{status:o,domElement:i}=e;if(o===n.synced||!i||t&&t!==e.containerId)return void(l=i);if(o===n.drain)return l=i.previousSibling,i?.remove(),void r.delete(s);if(e.status=n.synced,!this.isNotUniqueTag(s)){const e=document.querySelector(s);if(e)return l=i,void e.replaceWith(i)}const c=l&&l.nextSibling;c?a.insertBefore(i,c):a.append(i),l=i})),e.publish(o.SYNC_META,{})}pushTags(t,r,s=!0){const i=this.tags.containers.has(r);this.pushElements(t,r,s,i?n.synced:n.init),e.publish(o.PUSH_TAGS,{elements:t,containerId:r}),this.isServer||i||this.syncMeta()}removeTags(e){this.tags.meta.forEach(((t,r)=>{if(t.containerId===e&&t.containerId!==d.rootContainerId)return this.isNotUniqueTag(r)?(t.domElement?.remove(),void this.tags.meta.delete(r)):void(t.status=n.drain)})),e!==d.rootContainerId&&(this.tags.body.delete(e),this.tags.html.delete(e),this.tags.containers.delete(e),this.syncTimerId=setTimeout((()=>this.syncMeta()),500))}getDomElementProps(e){const t=Object.fromEntries(Array.from(e.attributes,(({name:e,value:t})=>[l.get(e)??e,!!(c.has(e)||["capture","download"].includes(e)&&""===t)||t])));return e.hasAttribute("style")&&(t.style=Object.fromEntries(Array.from(e.style,(t=>{const r=t.startsWith("--")?t:t.replace(/^-ms-/,"ms-").replace(/-([a-z])/g,((e,t)=>t.toUpperCase())),s=e.style.getPropertyPriority(t),o=e.style.getPropertyValue(t);return[r,s?`${o} !${s}`:o]})))),["title","style","script","noscript"].includes(e.tagName.toLowerCase())&&(t.children=e.textContent??""),t}analyzeClientHead(){if(this.isServer)return;for(const e of[document.documentElement,document.body])if(e){const t=e.tagName.toLowerCase();this.pushElement({type:t,props:this.getDomElementProps(e)},0,d.rootContainerId,!1,n.init),this.rootAttributes.has(e)||this.syncRootAttributes(e,new Map([[d.rootContainerId,this.tags[t].get(d.rootContainerId)]]),!0)}let e=0,t=!1;document.head?.childNodes.forEach((r=>{if(r.nodeType===Node.TEXT_NODE)return void(r.textContent&&(t||(e+=1),t=!0));if(t=!1,r.nodeType!==Node.ELEMENT_NODE)return;const s=r;this.pushElement({type:s.tagName.toLowerCase(),props:this.getDomElementProps(s)},e,d.rootContainerId,!1,n.synced,s),e+=1})),this.tags.containers.add(d.rootContainerId)}}export{d as default};
|
|
2
2
|
//# sourceMappingURL=manager.js.map
|
package/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sources":["../src/manager.ts"],"sourcesContent":["import EventManager from '@lomray/event-manager';\nimport htmlParser from 'html-react-parser';\nimport type { ReactElement, ReactNode } from 'react';\nimport React, { Fragment, Children } from 'react';\nimport Events from './events';\nimport TagStatus from './tag-status';\n\nexport interface IMetaManagerTags {\n html: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >;\n body: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >; // containerId => styles\n meta: Map<\n string,\n {\n element?: ReactElement;\n domElement?: HTMLElement;\n order: number;\n containerId: string;\n status: TagStatus;\n }\n >;\n containers: Set<string>;\n}\n\nexport interface IMetaManagerTagsDefinitions {\n [type: string]: {\n key?: string;\n order: number;\n };\n}\n\n/**\n * Meta tags manager\n */\nclass Manager {\n /**\n * Root container id\n */\n public static rootContainerId = 'root';\n\n /**\n * Detect server side\n */\n public isServer = typeof window === 'undefined';\n\n /**\n * Meta tags state\n */\n protected tags: IMetaManagerTags;\n\n /**\n * Tags definitions\n */\n protected tagsDefinitions: IMetaManagerTagsDefinitions = {\n 'meta[charset]': {\n order: 10,\n },\n \"meta[name='viewport']\": {\n order: 20,\n },\n title: {\n order: 100,\n },\n base: {\n order: 110,\n },\n meta: {\n order: 200,\n },\n link: {\n order: 300,\n },\n script: {\n order: 400,\n },\n noscript: {\n order: 500,\n },\n style: {\n order: 600,\n },\n };\n\n /**\n * System tag attributes\n */\n protected reservedAttributes = {\n order: 'data-order',\n };\n\n /**\n * Scheduled synchronization\n */\n protected syncTimerId: null | NodeJS.Timeout = null;\n\n /**\n * @constructor\n */\n constructor(tags: Partial<Manager['tags']> = {}) {\n this.tags = {\n html: new Map(tags?.html),\n body: new Map(tags?.body),\n meta: new Map(tags?.meta),\n containers: new Set(tags?.containers),\n };\n }\n\n /**\n * Set tags definitions\n */\n public setTagsDefinitions(definitions: IMetaManagerTagsDefinitions): void {\n this.tagsDefinitions = { ...this.tagsDefinitions, ...definitions };\n }\n\n /**\n * Remove children, dangerouslySetInnerHTML etc.\n */\n public static cleanupElementProps(props: Record<string, any>): Record<string, any> {\n const { children: _, dangerouslySetInnerHTML: __, ...restProps } = props;\n\n return restProps;\n }\n\n /**\n * Sort elements or props\n */\n protected sortTags<T extends Map<string, { order: number }>>(elements: T): T {\n return new Map(\n [...elements.entries()].sort(([, tagA], [, tagB]) => tagA.order - tagB.order),\n ) as T;\n }\n\n /**\n * Get meta tags\n */\n public getTags(): IMetaManagerTags {\n const { meta, body, html, containers } = this.tags;\n\n return {\n html: this.sortTags(html),\n body: this.sortTags(body),\n meta: this.sortTags(meta),\n containers,\n };\n }\n\n /**\n * Replace react attribute to valid DOM attribute\n */\n protected replaceAttribute(tagName: string, attribute: string): string {\n if (tagName === 'meta' && attribute.toLowerCase() === 'httpequiv') {\n return 'http-equiv';\n }\n\n return attribute;\n }\n\n /**\n * Create DOM element from React element\n */\n protected createDomElement(element: ReactElement): HTMLElement | undefined {\n if (this.isServer) {\n return;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { type, props } = element;\n const domElement = document.createElement(type as string);\n\n this.applyDomElementAttributes(domElement, props as Record<string, string>);\n\n return domElement;\n }\n\n /**\n * Make element key by element props\n */\n protected buildKeyByProps(\n type: string,\n props: Record<string, any> = {},\n isSkipProps = false,\n ): string | undefined {\n let key = '';\n\n // try to build unique key by unique props\n for (const uniqueAttr of ['id', 'name', 'property', 'href', 'src']) {\n if (props[uniqueAttr]) {\n key = `[${uniqueAttr}='${props[uniqueAttr] as string}']`;\n\n break;\n }\n }\n\n // try to build by props\n if (!key && !isSkipProps) {\n key = Object.entries(props)\n .map(([k, v]) => {\n if (typeof v === 'string') {\n return `[${k}]`;\n }\n\n return false;\n })\n .filter(Boolean)\n .join('');\n }\n\n if (!key) {\n return undefined;\n }\n\n return `${type}${key}`;\n }\n\n /**\n * Get default tag key for not unique elements\n */\n protected getDefaultKey(type: string, containerId: string, index: number): string {\n return `${type}-${containerId}-${index}-not-unique`;\n }\n\n /**\n * Check if tag is not unique\n */\n protected isNotUniqueTag(key: string): boolean {\n return key.endsWith('-not-unique');\n }\n\n /**\n * Clone react element\n */\n protected cloneElement(element: ReactElement): {\n element: ReactElement;\n elementProps: Record<string, any>;\n } {\n const { type } = element;\n const props = { ...(element?.props ?? {}) } as Record<string, any>;\n\n // remove system attributes\n Object.values(this.reservedAttributes).forEach((attrName) => {\n if (props[attrName]) {\n delete props[attrName];\n }\n });\n\n // fix multiple nodes for title\n if (type === 'title' && Array.isArray(props.children)) {\n props.children = props.children.join('');\n }\n\n return {\n element: React.createElement(type, props),\n elementProps: props,\n };\n }\n\n /**\n * Get element order\n */\n protected getElementOrder(\n elementProps: Record<string, any>,\n type: string,\n key: string = 'unknown',\n ): number {\n const elementOrder = elementProps[this.reservedAttributes.order]\n ? Number(elementProps[this.reservedAttributes.order])\n : undefined;\n\n return (\n elementOrder ?? this.tagsDefinitions[key]?.order ?? this.tagsDefinitions[type]?.order ?? 1000\n );\n }\n\n /**\n * Push react elements to meta state\n */\n protected pushElements(\n elements: ReactNode,\n containerId: string,\n isReplace = true,\n status = TagStatus.init,\n ): void {\n // unwrap fragment\n const clearElements: ReactNode =\n elements && typeof elements === 'object' && 'type' in elements && elements.type === Fragment\n ? ((elements.props as Record<string, any>).children as ReactNode)\n : elements;\n\n Children.forEach(clearElements, (child, index) => {\n // skip unsupported elements\n if (\n !child ||\n typeof child !== 'object' ||\n !('type' in child) ||\n !child.type ||\n typeof child.type !== 'string'\n ) {\n return;\n }\n\n const { type } = child;\n const { element, elementProps } = this.cloneElement(child);\n let key = this.tagsDefinitions[type]?.key ?? type;\n\n switch (type) {\n case 'title':\n break;\n\n case 'meta':\n const { charSet, httpEquiv } = elementProps;\n\n if (charSet) {\n key = `meta[charset]`;\n } else if (httpEquiv) {\n key = `meta[httpEquiv]`;\n } else {\n key = this.buildKeyByProps(type, elementProps)!;\n }\n\n break;\n\n case 'html':\n case 'body':\n if (!isReplace && this.tags[type].has(key)) {\n return;\n }\n\n this.tags[type].set(containerId, {\n props: Manager.cleanupElementProps(elementProps),\n order:\n containerId === Manager.rootContainerId\n ? 1\n : this.getElementOrder(elementProps, type, key),\n });\n\n return;\n\n case 'link':\n case 'script':\n case 'noscript':\n case 'style':\n default:\n key =\n this.buildKeyByProps(type, elementProps, true) ??\n this.getDefaultKey(type, containerId, index);\n break;\n }\n\n // skip replace existed meta tag (e.g. for push existed tags from parsed html, @see ServerManager)\n if (!isReplace && this.tags.meta.has(key)) {\n return;\n }\n\n // skip push already existed in head not unique tags\n if (this.isNotUniqueTag(key) && this.tags.meta.get(key)?.status === TagStatus.synced) {\n return;\n }\n\n this.tags.meta.set(key, {\n element: this.isServer ? element : undefined, // keep element only for server render\n domElement:\n /**\n * create DOM element only for client side\n * generate DOM element for root container inside @see this.analyzeClientHead\n */\n containerId === Manager.rootContainerId ? undefined : this.createDomElement(element),\n order: this.getElementOrder(elementProps, type, key),\n containerId,\n status,\n });\n });\n\n this.tags.containers.add(containerId);\n }\n\n /**\n * Get html or body props\n */\n public getRootTagProps(props: Manager['tags']['html']): Record<string, any> {\n if (!props.size) {\n return {};\n }\n\n return [...props.values()].reduce(\n (res, val) => ({\n ...res,\n ...val.props,\n }),\n {},\n );\n }\n\n /**\n * Apply props to dom element\n */\n protected applyDomElementAttributes(element: Element, props: Record<string, any> = {}): void {\n const tagName = element.tagName.toLowerCase();\n const reservedAttributes = Object.values(this.reservedAttributes);\n\n // apply attributes\n Object.entries(props).forEach(([name, value]) => {\n switch (name) {\n case 'children':\n element.innerHTML = value as string;\n\n return;\n\n case 'style':\n return Object.entries(value as Record<string, string>).forEach(\n ([styleName, styleValue]) => {\n // @ts-ignore\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n element['style'][styleName] = styleValue;\n },\n );\n }\n\n if (reservedAttributes.includes(name)) {\n return;\n }\n\n element.setAttribute(this.replaceAttribute(tagName, name), value as string);\n });\n }\n\n /**\n * Synchronize local meta tags with head meta tags\n */\n protected syncMeta(containerId?: string): void {\n const { meta, html, body } = this.getTags();\n\n if (this.syncTimerId) {\n clearTimeout(this.syncTimerId);\n }\n\n // apply html, body props\n for (const { name, value } of [\n { name: 'html', value: html },\n { name: 'body', value: body },\n ]) {\n const tagProps = this.getRootTagProps(value);\n const domElement = document.querySelector(name);\n const existAttributes = domElement?.getAttributeNames() ?? [];\n\n // remove drained props\n existAttributes.forEach((attrName) => {\n if (tagProps[attrName]) {\n return;\n }\n\n domElement!.removeAttribute(attrName);\n });\n\n if (!Object.keys(tagProps).length || !domElement) {\n continue;\n }\n\n this.applyDomElementAttributes(domElement, tagProps);\n }\n\n if (!meta.size || this.isServer) {\n return;\n }\n\n const head = document.getElementsByTagName('head')?.[0];\n let prevElement: HTMLElement | ChildNode | null | undefined;\n\n meta.forEach((metaTag, key) => {\n const { status, domElement } = metaTag;\n\n if (\n status === TagStatus.synced ||\n !domElement ||\n (containerId && containerId !== metaTag.containerId)\n ) {\n prevElement = domElement;\n\n return;\n }\n\n // remove tags\n if (status === TagStatus.drain) {\n prevElement = domElement.previousSibling;\n domElement?.remove();\n meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.synced;\n\n if (!this.isNotUniqueTag(key)) {\n const exist = document.querySelector(key);\n\n if (exist) {\n prevElement = domElement;\n exist.replaceWith(domElement);\n\n return;\n }\n }\n\n const beforeElement = prevElement && prevElement.nextSibling;\n\n if (beforeElement) {\n head.insertBefore(domElement, beforeElement);\n } else {\n head.append(domElement);\n }\n\n prevElement = domElement;\n });\n\n EventManager.publish(Events.SYNC_META, {});\n }\n\n /**\n * Push new meta tags\n */\n public pushTags(elements: ReactNode, containerId: string, isReplace = true): void {\n const isAdded = this.tags.containers.has(containerId);\n\n this.pushElements(\n elements,\n containerId,\n isReplace,\n isAdded ? TagStatus.synced : TagStatus.init,\n );\n\n EventManager.publish(Events.PUSH_TAGS, { elements, containerId });\n\n // skip sync already synced tags\n if (this.isServer || isAdded) {\n return;\n }\n\n this.syncMeta();\n }\n\n /**\n * Remove meta tags\n */\n public removeTags(containerId: string): void {\n this.tags.meta.forEach((metaTag, key) => {\n if (metaTag.containerId !== containerId || metaTag.containerId === Manager.rootContainerId) {\n return;\n }\n\n // remove immediately not unique tags\n if (this.isNotUniqueTag(key)) {\n metaTag.domElement?.remove();\n this.tags.meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.drain;\n });\n\n if (containerId === Manager.rootContainerId) {\n return;\n }\n\n this.tags.body.delete(containerId);\n this.tags.html.delete(containerId);\n this.tags.containers.delete(containerId);\n this.syncTimerId = setTimeout(() => this.syncMeta(), 500);\n }\n\n /**\n * Initial analyze client head meta tags\n */\n public analyzeClientHead(): void {\n if (this.isServer) {\n return;\n }\n\n // parse default attributes for root tags\n for (const tagName of ['html', 'body']) {\n // @ts-ignore\n const htmlTag = document.getElementsByTagName(tagName)?.[0].cloneNode(false)?.[\n 'outerHTML'\n ] as string;\n\n this.pushElements(htmlParser(htmlTag), Manager.rootContainerId, false);\n }\n\n // parse default meta tags\n const head = document.getElementsByTagName('head')?.[0];\n const reactElements = htmlParser(head?.innerHTML ?? '');\n\n this.pushElements(reactElements, Manager.rootContainerId, false, TagStatus.synced);\n\n const meta = [...this.tags.meta.values()];\n\n // attach real dom node to virtual tags\n head.childNodes.forEach((node, i) => {\n const existedElem = meta[i];\n\n if (!existedElem) {\n return;\n }\n\n existedElem.domElement = node as HTMLElement;\n });\n }\n}\n\nexport default Manager;\n"],"names":["Manager","static","isServer","window","tags","tagsDefinitions","order","title","base","meta","link","script","noscript","style","reservedAttributes","syncTimerId","constructor","this","html","Map","body","containers","Set","setTagsDefinitions","definitions","props","children","_","dangerouslySetInnerHTML","__","restProps","sortTags","elements","entries","sort","tagA","tagB","getTags","replaceAttribute","tagName","attribute","toLowerCase","createDomElement","element","type","domElement","document","createElement","applyDomElementAttributes","buildKeyByProps","isSkipProps","key","uniqueAttr","Object","map","k","v","filter","Boolean","join","getDefaultKey","containerId","index","isNotUniqueTag","endsWith","cloneElement","values","forEach","attrName","Array","isArray","React","elementProps","getElementOrder","Number","undefined","pushElements","isReplace","status","TagStatus","init","clearElements","Fragment","Children","child","charSet","httpEquiv","has","set","cleanupElementProps","rootContainerId","get","synced","add","getRootTagProps","size","reduce","res","val","name","value","innerHTML","styleName","styleValue","includes","setAttribute","syncMeta","clearTimeout","tagProps","querySelector","getAttributeNames","removeAttribute","keys","length","head","getElementsByTagName","prevElement","metaTag","drain","previousSibling","remove","delete","exist","replaceWith","beforeElement","nextSibling","insertBefore","append","EventManager","publish","Events","SYNC_META","pushTags","isAdded","PUSH_TAGS","removeTags","setTimeout","analyzeClientHead","htmlTag","cloneNode","htmlParser","reactElements","childNodes","node","i","existedElem"],"mappings":"kLA6CA,MAAMA,EAIGC,uBAAyB,OAKzBC,SAA6B,oBAAXC,OAKfC,KAKAC,gBAA+C,CACvD,gBAAiB,CACfC,MAAO,IAET,wBAAyB,CACvBA,MAAO,IAETC,MAAO,CACLD,MAAO,KAETE,KAAM,CACJF,MAAO,KAETG,KAAM,CACJH,MAAO,KAETI,KAAM,CACJJ,MAAO,KAETK,OAAQ,CACNL,MAAO,KAETM,SAAU,CACRN,MAAO,KAETO,MAAO,CACLP,MAAO,MAODQ,mBAAqB,CAC7BR,MAAO,cAMCS,YAAqC,KAK/CC,YAAYZ,EAAiC,IAC3Ca,KAAKb,KAAO,CACVc,KAAM,IAAIC,IAAIf,GAAMc,MACpBE,KAAM,IAAID,IAAIf,GAAMgB,MACpBX,KAAM,IAAIU,IAAIf,GAAMK,MACpBY,WAAY,IAAIC,IAAIlB,GAAMiB,YAE7B,CAKME,mBAAmBC,GACxBP,KAAKZ,gBAAkB,IAAKY,KAAKZ,mBAAoBmB,EACtD,CAKMvB,2BAA2BwB,GAChC,MAAQC,SAAUC,EAAGC,wBAAyBC,KAAOC,GAAcL,EAEnE,OAAOK,CACR,CAKSC,SAAmDC,GAC3D,OAAO,IAAIb,IACT,IAAIa,EAASC,WAAWC,MAAK,EAAC,CAAGC,IAAO,CAAGC,KAAUD,EAAK7B,MAAQ8B,EAAK9B,QAE1E,CAKM+B,UACL,MAAM5B,KAAEA,EAAIW,KAAEA,EAAIF,KAAEA,EAAIG,WAAEA,GAAeJ,KAAKb,KAE9C,MAAO,CACLc,KAAMD,KAAKc,SAASb,GACpBE,KAAMH,KAAKc,SAASX,GACpBX,KAAMQ,KAAKc,SAAStB,GACpBY,aAEH,CAKSiB,iBAAiBC,EAAiBC,GAC1C,MAAgB,SAAZD,GAAkD,cAA5BC,EAAUC,cAC3B,aAGFD,CACR,CAKSE,iBAAiBC,GACzB,GAAI1B,KAAKf,SACP,OAIF,MAAM0C,KAAEA,EAAInB,MAAEA,GAAUkB,EAClBE,EAAaC,SAASC,cAAcH,GAI1C,OAFA3B,KAAK+B,0BAA0BH,EAAYpB,GAEpCoB,CACR,CAKSI,gBACRL,EACAnB,EAA6B,CAAA,EAC7ByB,GAAc,GAEd,IAAIC,EAAM,GAGV,IAAK,MAAMC,IAAc,CAAC,KAAM,OAAQ,WAAY,OAAQ,OAC1D,GAAI3B,EAAM2B,GAAa,CACrBD,EAAM,IAAIC,MAAe3B,EAAM2B,OAE/B,KACD,CAiBH,GAbKD,GAAQD,IACXC,EAAME,OAAOpB,QAAQR,GAClB6B,KAAI,EAAEC,EAAGC,KACS,iBAANA,GACF,IAAID,OAKdE,OAAOC,SACPC,KAAK,KAGLR,EAIL,MAAO,GAAGP,IAAOO,GAClB,CAKSS,cAAchB,EAAciB,EAAqBC,GACzD,MAAO,GAAGlB,KAAQiB,KAAeC,cAClC,CAKSC,eAAeZ,GACvB,OAAOA,EAAIa,SAAS,cACrB,CAKSC,aAAatB,GAIrB,MAAMC,KAAEA,GAASD,EACXlB,EAAQ,IAAMkB,GAASlB,OAAS,CAAA,GActC,OAXA4B,OAAOa,OAAOjD,KAAKH,oBAAoBqD,SAASC,IAC1C3C,EAAM2C,WACD3C,EAAM2C,EACd,IAIU,UAATxB,GAAoByB,MAAMC,QAAQ7C,EAAMC,YAC1CD,EAAMC,SAAWD,EAAMC,SAASiC,KAAK,KAGhC,CACLhB,QAAS4B,EAAMxB,cAAcH,EAAMnB,GACnC+C,aAAc/C,EAEjB,CAKSgD,gBACRD,EACA5B,EACAO,EAAc,WAMd,OAJqBqB,EAAavD,KAAKH,mBAAmBR,OACtDoE,OAAOF,EAAavD,KAAKH,mBAAmBR,aAC5CqE,IAGc1D,KAAKZ,gBAAgB8C,IAAM7C,OAASW,KAAKZ,gBAAgBuC,IAAOtC,OAAS,GAE5F,CAKSsE,aACR5C,EACA6B,EACAgB,GAAY,EACZC,EAASC,EAAUC,MAGnB,MAAMC,EACJjD,GAAgC,iBAAbA,GAAyB,SAAUA,GAAYA,EAASY,OAASsC,EAC9ElD,EAASP,MAA8BC,SACzCM,EAENmD,EAAShB,QAAQc,GAAe,CAACG,EAAOtB,KAEtC,IACGsB,GACgB,iBAAVA,KACL,SAAUA,KACXA,EAAMxC,MACe,iBAAfwC,EAAMxC,KAEb,OAGF,MAAMA,KAAEA,GAASwC,GACXzC,QAAEA,EAAO6B,aAAEA,GAAiBvD,KAAKgD,aAAamB,GACpD,IAAIjC,EAAMlC,KAAKZ,gBAAgBuC,IAAOO,KAAOP,EAE7C,OAAQA,GACN,IAAK,QACH,MAEF,IAAK,OACH,MAAMyC,QAAEA,EAAOC,UAAEA,GAAcd,EAG7BrB,EADEkC,EACI,gBACGC,EACH,kBAEArE,KAAKgC,gBAAgBL,EAAM4B,GAGnC,MAEF,IAAK,OACL,IAAK,OACH,IAAKK,GAAa5D,KAAKb,KAAKwC,GAAM2C,IAAIpC,GACpC,OAWF,YARAlC,KAAKb,KAAKwC,GAAM4C,IAAI3B,EAAa,CAC/BpC,MAAOzB,EAAQyF,oBAAoBjB,GACnClE,MACEuD,IAAgB7D,EAAQ0F,gBACpB,EACAzE,KAAKwD,gBAAgBD,EAAc5B,EAAMO,KASnD,QACEA,EACElC,KAAKgC,gBAAgBL,EAAM4B,GAAc,IACzCvD,KAAK2C,cAAchB,EAAMiB,EAAaC,IAKvCe,GAAa5D,KAAKb,KAAKK,KAAK8E,IAAIpC,IAKjClC,KAAK8C,eAAeZ,IAAQlC,KAAKb,KAAKK,KAAKkF,IAAIxC,IAAM2B,SAAWC,EAAUa,QAI9E3E,KAAKb,KAAKK,KAAK+E,IAAIrC,EAAK,CACtBR,QAAS1B,KAAKf,SAAWyC,OAAUgC,EACnC9B,WAKEgB,IAAgB7D,EAAQ0F,qBAAkBf,EAAY1D,KAAKyB,iBAAiBC,GAC9ErC,MAAOW,KAAKwD,gBAAgBD,EAAc5B,EAAMO,GAChDU,cACAiB,UACA,IAGJ7D,KAAKb,KAAKiB,WAAWwE,IAAIhC,EAC1B,CAKMiC,gBAAgBrE,GACrB,OAAKA,EAAMsE,KAIJ,IAAItE,EAAMyC,UAAU8B,QACzB,CAACC,EAAKC,KAAS,IACVD,KACAC,EAAIzE,SAET,CAAE,GARK,EAUV,CAKSuB,0BAA0BL,EAAkBlB,EAA6B,IACjF,MAAMc,EAAUI,EAAQJ,QAAQE,cAC1B3B,EAAqBuC,OAAOa,OAAOjD,KAAKH,oBAG9CuC,OAAOpB,QAAQR,GAAO0C,SAAQ,EAAEgC,EAAMC,MACpC,OAAQD,GACN,IAAK,WAGH,YAFAxD,EAAQ0D,UAAYD,GAItB,IAAK,QACH,OAAO/C,OAAOpB,QAAQmE,GAAiCjC,SACrD,EAAEmC,EAAWC,MAGX5D,EAAe,MAAE2D,GAAaC,CAAU,IAK5CzF,EAAmB0F,SAASL,IAIhCxD,EAAQ8D,aAAaxF,KAAKqB,iBAAiBC,EAAS4D,GAAOC,EAAgB,GAE9E,CAKSM,SAAS7C,GACjB,MAAMpD,KAAEA,EAAIS,KAAEA,EAAIE,KAAEA,GAASH,KAAKoB,UAE9BpB,KAAKF,aACP4F,aAAa1F,KAAKF,aAIpB,IAAK,MAAMoF,KAAEA,EAAIC,MAAEA,IAAW,CAC5B,CAAED,KAAM,OAAQC,MAAOlF,GACvB,CAAEiF,KAAM,OAAQC,MAAOhF,IACtB,CACD,MAAMwF,EAAW3F,KAAK6E,gBAAgBM,GAChCvD,EAAaC,SAAS+D,cAAcV,IAClBtD,GAAYiE,qBAAuB,IAG3C3C,SAASC,IACnBwC,EAASxC,IAIbvB,EAAYkE,gBAAgB3C,EAAS,IAGlCf,OAAO2D,KAAKJ,GAAUK,QAAWpE,GAItC5B,KAAK+B,0BAA0BH,EAAY+D,EAC5C,CAED,IAAKnG,EAAKsF,MAAQ9E,KAAKf,SACrB,OAGF,MAAMgH,EAAOpE,SAASqE,qBAAqB,UAAU,GACrD,IAAIC,EAEJ3G,EAAK0D,SAAQ,CAACkD,EAASlE,KACrB,MAAM2B,OAAEA,EAAMjC,WAAEA,GAAewE,EAE/B,GACEvC,IAAWC,EAAUa,SACpB/C,GACAgB,GAAeA,IAAgBwD,EAAQxD,YAIxC,YAFAuD,EAAcvE,GAMhB,GAAIiC,IAAWC,EAAUuC,MAKvB,OAJAF,EAAcvE,EAAW0E,gBACzB1E,GAAY2E,cACZ/G,EAAKgH,OAAOtE,GAOd,GAFAkE,EAAQvC,OAASC,EAAUa,QAEtB3E,KAAK8C,eAAeZ,GAAM,CAC7B,MAAMuE,EAAQ5E,SAAS+D,cAAc1D,GAErC,GAAIuE,EAIF,OAHAN,EAAcvE,OACd6E,EAAMC,YAAY9E,EAIrB,CAED,MAAM+E,EAAgBR,GAAeA,EAAYS,YAE7CD,EACFV,EAAKY,aAAajF,EAAY+E,GAE9BV,EAAKa,OAAOlF,GAGduE,EAAcvE,CAAU,IAG1BmF,EAAaC,QAAQC,EAAOC,UAAW,CAAE,EAC1C,CAKMC,SAASpG,EAAqB6B,EAAqBgB,GAAY,GACpE,MAAMwD,EAAUpH,KAAKb,KAAKiB,WAAWkE,IAAI1B,GAEzC5C,KAAK2D,aACH5C,EACA6B,EACAgB,EACAwD,EAAUtD,EAAUa,OAASb,EAAUC,MAGzCgD,EAAaC,QAAQC,EAAOI,UAAW,CAAEtG,WAAU6B,gBAG/C5C,KAAKf,UAAYmI,GAIrBpH,KAAKyF,UACN,CAKM6B,WAAW1E,GAChB5C,KAAKb,KAAKK,KAAK0D,SAAQ,CAACkD,EAASlE,KAC/B,GAAIkE,EAAQxD,cAAgBA,GAAewD,EAAQxD,cAAgB7D,EAAQ0F,gBAK3E,OAAIzE,KAAK8C,eAAeZ,IACtBkE,EAAQxE,YAAY2E,cACpBvG,KAAKb,KAAKK,KAAKgH,OAAOtE,SAKxBkE,EAAQvC,OAASC,EAAUuC,MAAK,IAG9BzD,IAAgB7D,EAAQ0F,kBAI5BzE,KAAKb,KAAKgB,KAAKqG,OAAO5D,GACtB5C,KAAKb,KAAKc,KAAKuG,OAAO5D,GACtB5C,KAAKb,KAAKiB,WAAWoG,OAAO5D,GAC5B5C,KAAKF,YAAcyH,YAAW,IAAMvH,KAAKyF,YAAY,KACtD,CAKM+B,oBACL,GAAIxH,KAAKf,SACP,OAIF,IAAK,MAAMqC,IAAW,CAAC,OAAQ,QAAS,CAEtC,MAAMmG,EAAU5F,SAASqE,qBAAqB5E,KAAW,GAAGoG,WAAU,IACzD,UAGb1H,KAAK2D,aAAagE,EAAWF,GAAU1I,EAAQ0F,iBAAiB,EACjE,CAGD,MAAMwB,EAAOpE,SAASqE,qBAAqB,UAAU,GAC/C0B,EAAgBD,EAAW1B,GAAMb,WAAa,IAEpDpF,KAAK2D,aAAaiE,EAAe7I,EAAQ0F,iBAAiB,EAAOX,EAAUa,QAE3E,MAAMnF,EAAO,IAAIQ,KAAKb,KAAKK,KAAKyD,UAGhCgD,EAAK4B,WAAW3E,SAAQ,CAAC4E,EAAMC,KAC7B,MAAMC,EAAcxI,EAAKuI,GAEpBC,IAILA,EAAYpG,WAAakG,EAAmB,GAE/C"}
|
|
1
|
+
{"version":3,"file":"manager.js","sources":["../src/manager.ts"],"sourcesContent":["import EventManager from '@lomray/event-manager';\nimport type { ReactElement, ReactNode } from 'react';\nimport React, { Fragment, Children } from 'react';\nimport Events from './events';\nimport RootAttributes from './root-attributes';\nimport TagStatus from './tag-status';\n\n// One source of truth for React prop names and their HTML attribute names.\nconst attributeNames = new Map([\n ['className', 'class'],\n ['htmlFor', 'for'],\n ['httpEquiv', 'http-equiv'],\n ['charSet', 'charset'],\n ['crossOrigin', 'crossorigin'],\n ['referrerPolicy', 'referrerpolicy'],\n ['tabIndex', 'tabindex'],\n ['hrefLang', 'hreflang'],\n ['imageSrcSet', 'imagesrcset'],\n ['imageSizes', 'imagesizes'],\n ['fetchPriority', 'fetchpriority'],\n ['noModule', 'nomodule'],\n ['srcSet', 'srcset'],\n ['acceptCharset', 'accept-charset'],\n ['itemProp', 'itemprop'],\n ['itemScope', 'itemscope'],\n ['itemType', 'itemtype'],\n ['itemID', 'itemid'],\n ['itemRef', 'itemref'],\n]);\nconst propNames = new Map([...attributeNames].map(([prop, attribute]) => [attribute, prop]));\n\n// Presence means true for HTML boolean attributes, regardless of their text value.\nconst booleanAttributes = new Set([\n 'allowfullscreen',\n 'async',\n 'autofocus',\n 'autoplay',\n 'checked',\n 'controls',\n 'default',\n 'defer',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'formnovalidate',\n 'hidden',\n 'itemscope',\n 'loop',\n 'multiple',\n 'muted',\n 'nomodule',\n 'novalidate',\n 'open',\n 'playsinline',\n 'readonly',\n 'required',\n 'reversed',\n 'scoped',\n 'seamless',\n 'selected',\n]);\n\nexport interface IMetaManagerTags {\n html: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >;\n body: Map<\n string,\n {\n props: Record<string, any>;\n order: number;\n }\n >; // containerId => styles\n meta: Map<\n string,\n {\n element?: ReactElement;\n domElement?: HTMLElement;\n order: number;\n containerId: string;\n status: TagStatus;\n }\n >;\n containers: Set<string>;\n}\n\nexport interface IMetaManagerTagsDefinitions {\n [type: string]: {\n key?: string;\n order: number;\n };\n}\n\n/**\n * Meta tags manager\n */\nclass Manager {\n /**\n * Root container id\n */\n public static rootContainerId = 'root';\n\n /**\n * Detect server side\n */\n public isServer = typeof window === 'undefined';\n\n /**\n * Meta tags state\n */\n protected tags: IMetaManagerTags;\n\n /**\n * Tags definitions\n */\n protected tagsDefinitions: IMetaManagerTagsDefinitions = {\n 'meta[charset]': {\n order: 10,\n },\n \"meta[name='viewport']\": {\n order: 20,\n },\n title: {\n order: 100,\n },\n base: {\n order: 110,\n },\n meta: {\n order: 200,\n },\n link: {\n order: 300,\n },\n script: {\n order: 400,\n },\n noscript: {\n order: 500,\n },\n style: {\n order: 600,\n },\n };\n\n /**\n * System tag attributes\n */\n protected reservedAttributes = {\n order: 'data-order',\n };\n\n /**\n * Scheduled synchronization\n */\n protected syncTimerId: null | NodeJS.Timeout = null;\n\n /** DOM ownership is private client state, separate from the public tag snapshot. */\n private rootAttributes = new WeakMap<HTMLElement, RootAttributes>();\n\n /**\n * @constructor\n */\n constructor(tags: Partial<Manager['tags']> = {}) {\n this.tags = {\n html: new Map(tags?.html),\n body: new Map(tags?.body),\n meta: new Map(tags?.meta),\n containers: new Set(tags?.containers),\n };\n }\n\n /**\n * Set tags definitions\n */\n public setTagsDefinitions(definitions: IMetaManagerTagsDefinitions): void {\n this.tagsDefinitions = { ...this.tagsDefinitions, ...definitions };\n }\n\n /**\n * Remove children, dangerouslySetInnerHTML etc.\n */\n public static cleanupElementProps(props: Record<string, any>): Record<string, any> {\n const { children: _, dangerouslySetInnerHTML: __, ...restProps } = props;\n\n return restProps;\n }\n\n /**\n * Sort elements or props\n */\n protected sortTags<T extends Map<string, { order: number }>>(elements: T): T {\n return new Map(\n [...elements.entries()].sort(([, tagA], [, tagB]) => tagA.order - tagB.order),\n ) as T;\n }\n\n /**\n * Get meta tags\n */\n public getTags(): IMetaManagerTags {\n const { meta, body, html, containers } = this.tags;\n\n return {\n html: this.sortTags(html),\n body: this.sortTags(body),\n meta: this.sortTags(meta),\n containers,\n };\n }\n\n /**\n * Replace react attribute to valid DOM attribute\n */\n protected replaceAttribute(tagName: string, attribute: string): string {\n return attributeNames.get(attribute) ?? attribute;\n }\n\n /**\n * Create DOM element from React element\n */\n protected createDomElement(element: ReactElement): HTMLElement | undefined {\n if (this.isServer) {\n return;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { type, props } = element;\n const domElement = document.createElement(type as string);\n\n this.applyDomElementAttributes(domElement, props as Record<string, string>);\n\n return domElement;\n }\n\n /**\n * Make element key by element props\n */\n protected buildKeyByProps(\n type: string,\n props: Record<string, any> = {},\n isSkipProps = false,\n ): string | undefined {\n let key = '';\n\n // try to build unique key by unique props\n for (const uniqueAttr of ['id', 'name', 'property', 'href', 'src']) {\n if (props[uniqueAttr]) {\n key = `[${uniqueAttr}='${props[uniqueAttr] as string}']`;\n\n break;\n }\n }\n\n // try to build by props\n if (!key && !isSkipProps) {\n key = Object.entries(props)\n .map(([k, v]) => {\n if (typeof v === 'string') {\n return `[${k}]`;\n }\n\n return false;\n })\n .filter(Boolean)\n .join('');\n }\n\n if (!key) {\n return undefined;\n }\n\n return `${type}${key}`;\n }\n\n /**\n * Get default tag key for not unique elements\n */\n protected getDefaultKey(type: string, containerId: string, index: number): string {\n return `${type}-${containerId}-${index}-not-unique`;\n }\n\n /**\n * Check if tag is not unique\n */\n protected isNotUniqueTag(key: string): boolean {\n return key.endsWith('-not-unique');\n }\n\n /**\n * Clone react element\n */\n protected cloneElement(element: Pick<ReactElement, 'type' | 'props'>): {\n element: ReactElement;\n elementProps: Record<string, any>;\n } {\n const { type } = element;\n const props = { ...(element?.props ?? {}) } as Record<string, any>;\n\n // remove system attributes\n Object.values(this.reservedAttributes).forEach((attrName) => {\n if (props[attrName]) {\n delete props[attrName];\n }\n });\n\n // fix multiple nodes for title\n if (type === 'title' && Array.isArray(props.children)) {\n props.children = props.children.join('');\n }\n\n return {\n element: React.createElement(type, props),\n elementProps: props,\n };\n }\n\n /**\n * Get element order\n */\n protected getElementOrder(\n elementProps: Record<string, any>,\n type: string,\n key: string = 'unknown',\n ): number {\n const elementOrder = elementProps[this.reservedAttributes.order]\n ? Number(elementProps[this.reservedAttributes.order])\n : undefined;\n\n return (\n elementOrder ?? this.tagsDefinitions[key]?.order ?? this.tagsDefinitions[type]?.order ?? 1000\n );\n }\n\n /**\n * Push react elements to meta state\n */\n protected pushElements(\n elements: ReactNode,\n containerId: string,\n isReplace = true,\n status = TagStatus.init,\n ): void {\n // unwrap fragment\n const clearElements: ReactNode =\n elements && typeof elements === 'object' && 'type' in elements && elements.type === Fragment\n ? ((elements.props as Record<string, any>).children as ReactNode)\n : elements;\n\n Children.forEach(clearElements, (child, index) => {\n // skip unsupported elements\n if (\n !child ||\n typeof child !== 'object' ||\n !('type' in child) ||\n !child.type ||\n typeof child.type !== 'string'\n ) {\n return;\n }\n\n this.pushElement(\n { type: child.type, props: child.props as Record<string, any> },\n index,\n containerId,\n isReplace,\n status,\n );\n });\n\n this.tags.containers.add(containerId);\n }\n\n /**\n * Register a React element or a snapshot of a live DOM element.\n */\n protected pushElement(\n child: { type: string; props: Record<string, any> },\n index: number,\n containerId: string,\n isReplace: boolean,\n status: TagStatus,\n domElement?: HTMLElement,\n ): void {\n const { type } = child;\n const { element, elementProps } = this.cloneElement(child);\n let key = this.tagsDefinitions[type]?.key ?? type;\n\n switch (type) {\n case 'title':\n break;\n\n case 'meta':\n const { charSet, httpEquiv } = elementProps;\n\n if (charSet) {\n key = `meta[charset]`;\n } else if (httpEquiv) {\n key = `meta[httpEquiv]`;\n } else {\n key = this.buildKeyByProps(type, elementProps)!;\n }\n\n break;\n\n case 'html':\n case 'body':\n if (!isReplace && this.tags[type].has(key)) {\n return;\n }\n\n this.tags[type].set(containerId, {\n props: Manager.cleanupElementProps(elementProps),\n order:\n containerId === Manager.rootContainerId\n ? 1\n : this.getElementOrder(elementProps, type, key),\n });\n\n return;\n\n case 'link':\n case 'script':\n case 'noscript':\n case 'style':\n default:\n key =\n this.buildKeyByProps(type, elementProps, true) ??\n this.getDefaultKey(type, containerId, index);\n break;\n }\n\n // skip replace existed meta tag (e.g. for push existed tags from parsed html, @see ServerManager)\n const existedTag = this.tags.meta.get(key);\n\n if (!isReplace && existedTag) {\n // Hydration may register a detached node before analysis. Keep the first live duplicate.\n if (domElement && existedTag.domElement?.parentNode !== domElement.parentNode) {\n existedTag.domElement = domElement;\n }\n\n return;\n }\n\n // skip push already existed in head not unique tags\n if (this.isNotUniqueTag(key) && existedTag?.status === TagStatus.synced) {\n return;\n }\n\n this.tags.meta.set(key, {\n element: this.isServer ? element : undefined, // keep element only for server render\n domElement:\n /**\n * create DOM element only for client side\n * generate DOM element for root container inside @see this.analyzeClientHead\n */\n domElement ??\n (containerId === Manager.rootContainerId ? undefined : this.createDomElement(element)),\n order: this.getElementOrder(elementProps, type, key),\n containerId,\n status,\n });\n }\n\n /**\n * Get html or body props\n */\n public getRootTagProps(props: Manager['tags']['html']): Record<string, any> {\n if (!props.size) {\n return {};\n }\n\n return [...props.values()].reduce(\n (res, val) => ({\n ...res,\n ...val.props,\n }),\n {},\n );\n }\n\n /**\n * Apply props to dom element\n */\n protected applyDomElementAttributes(element: Element, props: Record<string, any> = {}): void {\n const tagName = element.tagName.toLowerCase();\n const reservedAttributes = Object.values(this.reservedAttributes);\n\n // apply attributes\n Object.entries(props).forEach(([name, value]) => {\n if (reservedAttributes.includes(name)) {\n return;\n }\n\n if (name === 'children') {\n element.innerHTML = value === false || value == null ? '' : (value as string);\n\n return;\n }\n\n const attribute = this.replaceAttribute(tagName, name);\n\n if (value === false || value == null) {\n element.removeAttribute(attribute);\n\n return;\n }\n\n if (name === 'style' && typeof value === 'object') {\n return Object.entries(value as Record<string, string>).forEach(\n ([styleName, styleValue]) => {\n // @ts-ignore\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n element['style'][styleName] = styleValue;\n },\n );\n }\n\n element.setAttribute(attribute, value === true ? '' : (value as string));\n });\n }\n\n /** Merge root style objects while retaining ownership of each contributed property. */\n private syncRootAttributes(\n element: HTMLElement,\n tags: IMetaManagerTags['html'],\n seed = false,\n ): void {\n const props = this.getRootTagProps(tags);\n const attributes = new Map(\n Object.entries(props)\n .filter(([name]) => !Object.values(this.reservedAttributes).includes(name))\n .map(([name, value]) => [\n this.replaceAttribute(element.tagName.toLowerCase(), name),\n value === false || value == null ? null : value === true ? '' : String(value),\n ]),\n );\n const style = [...tags.values()].reduce<Record<string, unknown>>(\n (result, { props: tagProps }) => {\n if (!('style' in tagProps)) {\n return result;\n }\n\n return tagProps.style && typeof tagProps.style === 'object'\n ? { ...result, ...(tagProps.style as Record<string, unknown>) }\n : {};\n },\n {},\n );\n let ownership = this.rootAttributes.get(element);\n\n if (!ownership) {\n ownership = new RootAttributes();\n this.rootAttributes.set(element, ownership);\n }\n\n ownership.sync(element, attributes, style, seed);\n }\n\n /**\n * Synchronize local meta tags with head meta tags\n */\n protected syncMeta(containerId?: string): void {\n const { meta, html, body } = this.getTags();\n\n if (this.syncTimerId) {\n clearTimeout(this.syncTimerId);\n }\n\n // apply html, body props\n for (const { name, value } of [\n { name: 'html', value: html },\n { name: 'body', value: body },\n ]) {\n const domElement = document.querySelector<HTMLElement>(name);\n\n if (domElement) {\n this.syncRootAttributes(domElement, value);\n }\n }\n\n if (!meta.size || this.isServer) {\n return;\n }\n\n const head = document.getElementsByTagName('head')?.[0];\n let prevElement: HTMLElement | ChildNode | null | undefined;\n\n meta.forEach((metaTag, key) => {\n const { status, domElement } = metaTag;\n\n if (\n status === TagStatus.synced ||\n !domElement ||\n (containerId && containerId !== metaTag.containerId)\n ) {\n prevElement = domElement;\n\n return;\n }\n\n // remove tags\n if (status === TagStatus.drain) {\n prevElement = domElement.previousSibling;\n domElement?.remove();\n meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.synced;\n\n if (!this.isNotUniqueTag(key)) {\n const exist = document.querySelector(key);\n\n if (exist) {\n prevElement = domElement;\n exist.replaceWith(domElement);\n\n return;\n }\n }\n\n const beforeElement = prevElement && prevElement.nextSibling;\n\n if (beforeElement) {\n head.insertBefore(domElement, beforeElement);\n } else {\n head.append(domElement);\n }\n\n prevElement = domElement;\n });\n\n EventManager.publish(Events.SYNC_META, {});\n }\n\n /**\n * Push new meta tags\n */\n public pushTags(elements: ReactNode, containerId: string, isReplace = true): void {\n const isAdded = this.tags.containers.has(containerId);\n\n this.pushElements(\n elements,\n containerId,\n isReplace,\n isAdded ? TagStatus.synced : TagStatus.init,\n );\n\n EventManager.publish(Events.PUSH_TAGS, { elements, containerId });\n\n // skip sync already synced tags\n if (this.isServer || isAdded) {\n return;\n }\n\n this.syncMeta();\n }\n\n /**\n * Remove meta tags\n */\n public removeTags(containerId: string): void {\n this.tags.meta.forEach((metaTag, key) => {\n if (metaTag.containerId !== containerId || metaTag.containerId === Manager.rootContainerId) {\n return;\n }\n\n // remove immediately not unique tags\n if (this.isNotUniqueTag(key)) {\n metaTag.domElement?.remove();\n this.tags.meta.delete(key);\n\n return;\n }\n\n metaTag.status = TagStatus.drain;\n });\n\n if (containerId === Manager.rootContainerId) {\n return;\n }\n\n this.tags.body.delete(containerId);\n this.tags.html.delete(containerId);\n this.tags.containers.delete(containerId);\n this.syncTimerId = setTimeout(() => this.syncMeta(), 500);\n }\n\n /**\n * Read attributes and text from the live DOM without serializing markup.\n */\n protected getDomElementProps(element: HTMLElement): Record<string, any> {\n const props: Record<string, any> = Object.fromEntries(\n Array.from(element.attributes, ({ name, value }) => [\n propNames.get(name) ?? name,\n booleanAttributes.has(name) || (['capture', 'download'].includes(name) && value === '')\n ? true\n : value,\n ]),\n );\n\n if (element.hasAttribute('style')) {\n // The DOM already parsed inline CSS; keep the React style object used by root tags.\n props.style = Object.fromEntries(\n Array.from(element.style, (name) => {\n const propName = name.startsWith('--')\n ? name\n : name\n .replace(/^-ms-/, 'ms-')\n .replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());\n const priority = element.style.getPropertyPriority(name);\n const value = element.style.getPropertyValue(name);\n\n return [propName, priority ? `${value} !${priority}` : value];\n }),\n );\n }\n\n if (['title', 'style', 'script', 'noscript'].includes(element.tagName.toLowerCase())) {\n props.children = element.textContent ?? '';\n }\n\n return props;\n }\n\n /**\n * Initial analyze client head meta tags\n */\n public analyzeClientHead(): void {\n if (this.isServer) {\n return;\n }\n\n for (const element of [document.documentElement, document.body]) {\n if (element) {\n const type = element.tagName.toLowerCase() as 'html' | 'body';\n\n this.pushElement(\n { type, props: this.getDomElementProps(element) },\n 0,\n Manager.rootContainerId,\n false,\n TagStatus.init,\n );\n\n if (!this.rootAttributes.has(element)) {\n this.syncRootAttributes(\n element,\n new Map([[Manager.rootContainerId, this.tags[type].get(Manager.rootContainerId)!]]),\n true,\n );\n }\n }\n }\n\n let index = 0;\n let isPreviousText = false;\n\n document.head?.childNodes.forEach((node) => {\n // Text occupied React child positions in the old snapshot, but never made tags.\n // Adjacent text nodes serialize as one; comments separate them without taking a position.\n if (node.nodeType === Node.TEXT_NODE) {\n if (node.textContent) {\n if (!isPreviousText) {\n index += 1;\n }\n\n isPreviousText = true;\n }\n\n return;\n }\n\n isPreviousText = false;\n\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return;\n }\n\n const element = node as HTMLElement;\n\n this.pushElement(\n { type: element.tagName.toLowerCase(), props: this.getDomElementProps(element) },\n index,\n Manager.rootContainerId,\n false,\n TagStatus.synced,\n element,\n );\n index += 1;\n });\n\n this.tags.containers.add(Manager.rootContainerId);\n }\n}\n\nexport default Manager;\n"],"names":["attributeNames","Map","propNames","map","prop","attribute","booleanAttributes","Set","Manager","static","isServer","window","tags","tagsDefinitions","order","title","base","meta","link","script","noscript","style","reservedAttributes","syncTimerId","rootAttributes","WeakMap","constructor","this","html","body","containers","setTagsDefinitions","definitions","props","children","_","dangerouslySetInnerHTML","__","restProps","sortTags","elements","entries","sort","tagA","tagB","getTags","replaceAttribute","tagName","get","createDomElement","element","type","domElement","document","createElement","applyDomElementAttributes","buildKeyByProps","isSkipProps","key","uniqueAttr","Object","k","v","filter","Boolean","join","getDefaultKey","containerId","index","isNotUniqueTag","endsWith","cloneElement","values","forEach","attrName","Array","isArray","React","elementProps","getElementOrder","Number","undefined","pushElements","isReplace","status","TagStatus","init","clearElements","Fragment","Children","child","pushElement","add","charSet","httpEquiv","has","set","cleanupElementProps","rootContainerId","existedTag","synced","parentNode","getRootTagProps","size","reduce","res","val","toLowerCase","name","value","includes","innerHTML","styleName","styleValue","setAttribute","removeAttribute","syncRootAttributes","seed","attributes","String","result","tagProps","ownership","RootAttributes","sync","syncMeta","clearTimeout","querySelector","head","getElementsByTagName","prevElement","metaTag","drain","previousSibling","remove","delete","exist","replaceWith","beforeElement","nextSibling","insertBefore","append","EventManager","publish","Events","SYNC_META","pushTags","isAdded","PUSH_TAGS","removeTags","setTimeout","getDomElementProps","fromEntries","from","hasAttribute","propName","startsWith","replace","letter","toUpperCase","priority","getPropertyPriority","getPropertyValue","textContent","analyzeClientHead","documentElement","isPreviousText","childNodes","node","nodeType","Node","TEXT_NODE","ELEMENT_NODE"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lomray/react-head-manager",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "React meta tags manager with SSR and Suspense support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
|
+
"sideEffects": false,
|
|
8
9
|
"keywords": [
|
|
9
10
|
"js",
|
|
10
11
|
"meta",
|
|
@@ -36,7 +37,8 @@
|
|
|
36
37
|
"lint:check": "eslint \"src/**/*.{ts,tsx,*.ts,*tsx}\"",
|
|
37
38
|
"lint:format": "eslint --fix \"src/**/*.{ts,tsx,*.ts,*tsx}\"",
|
|
38
39
|
"ts:check": "tsc --project ./tsconfig.json --skipLibCheck --noemit",
|
|
39
|
-
"test": "vitest run"
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:package": "node scripts/test-package.mjs"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
44
|
"html-react-parser": "^5.1.15"
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
"@types/sinon-chai": "^3.2.12",
|
|
54
56
|
"@vitest/coverage-v8": "^2.0.5",
|
|
55
57
|
"eslint": "^8.57.0",
|
|
58
|
+
"html-react-parser": "^5.1.15",
|
|
56
59
|
"husky": "^9.1.5",
|
|
57
60
|
"jsdom": "^25.0.0",
|
|
58
61
|
"lint-staged": "^15.2.10",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Values = Map<string, string | null>;
|
|
2
|
+
/** Track contributions, including individual class tokens and CSS properties. */
|
|
3
|
+
declare class RootAttributes {
|
|
4
|
+
private attributes;
|
|
5
|
+
private classes;
|
|
6
|
+
private styles;
|
|
7
|
+
/** Unchanged props must not overwrite changes made by other code since our last write. */
|
|
8
|
+
/** Unchanged props must not overwrite changes made by other code since our last write. */
|
|
9
|
+
private syncValues;
|
|
10
|
+
/** Seed from the initial snapshot without writing to the DOM. */
|
|
11
|
+
/** Seed from the initial snapshot without writing to the DOM. */
|
|
12
|
+
sync(element: HTMLElement, attributes: Values, style: Record<string, unknown>, seed?: boolean): void;
|
|
13
|
+
}
|
|
14
|
+
export { RootAttributes as default };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
class e{attributes=new Map;classes=new Map;styles=new Map;syncValues(e,t,s,l,a,n=s){for(const r of new Set([...e.keys(),...t.keys()])){const i=e.get(r),u=t.get(r)??null,c=s(r);if(t.has(r)){if(i)i.requested!==u&&(c===i.applied&&(l(r,u),i.applied=s(r)),i.requested=u);else if(null!==u){const t=a?null:n(r);a||l(r,u),e.set(r,{requested:u,applied:s(r),previous:t})}}else i&&c===i.applied&&l(r,i.previous),e.delete(r)}}sync(e,t,s,l=!1){const a=new Map(["class","style"].map((t=>[t,e.hasAttribute(t)?"":null]))),n=t.get("class"),r=new Map((n?.match(/\S+/g)??[]).map((e=>[e,""]))),i=new Map(Object.entries(s).map((([e,t])=>[e.startsWith("--")?e:e.replace(/[A-Z]/g,(e=>`-${e.toLowerCase()}`)).replace(/^ms-/,"-ms-").replace(/^css-float$/,"float"),!1===t||null==t?null:String(t)])));for(const e of["class","style"]){null!=t.get(e)&&t.set(e,"")}this.syncValues(this.classes,r,(t=>e.classList.contains(t)?"":null),((t,s)=>e.classList.toggle(t,null!==s)),l),this.syncValues(this.styles,i,(t=>{const s=e.style.getPropertyValue(t),l=e.style.getPropertyPriority(t);return s?`${s}${l?` !${l}`:""}`:null}),((t,s)=>{const l=/\s*!important\s*$/i;e.style.setProperty(t,s?.replace(l,"")??"",l.test(s??"")?"important":"")}),l),this.syncValues(this.attributes,t,(t=>{const s=e.getAttribute(t);return["class","style"].includes(t)&&null!==s?"":s}),((t,s)=>{["class","style"].includes(t)&&e.getAttribute(t)||(null===s?e.removeAttribute(t):e.setAttribute(t,s))}),l,(t=>a.has(t)?a.get(t):e.getAttribute(t)))}}export{e as default};
|
|
2
|
+
//# sourceMappingURL=root-attributes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-attributes.js","sources":["../src/root-attributes.ts"],"sourcesContent":["interface IOwnedValue {\n requested: string | null;\n applied: string | null;\n previous: string | null;\n}\n\ntype Values = Map<string, string | null>;\ntype Ownership = Map<string, IOwnedValue>;\n\n/** Track contributions, including individual class tokens and CSS properties. */\nclass RootAttributes {\n private attributes: Ownership = new Map();\n\n private classes: Ownership = new Map();\n\n private styles: Ownership = new Map();\n\n /** Unchanged props must not overwrite changes made by other code since our last write. */\n private syncValues(\n owned: Ownership,\n desired: Values,\n read: (name: string) => string | null,\n write: (name: string, value: string | null) => void,\n seed: boolean,\n previous: (name: string) => string | null = read,\n ): void {\n for (const name of new Set([...owned.keys(), ...desired.keys()])) {\n const prior = owned.get(name);\n const requested = desired.get(name) ?? null;\n const current = read(name);\n\n if (!desired.has(name)) {\n if (prior && current === prior.applied) {\n write(name, prior.previous);\n }\n\n owned.delete(name);\n } else if (!prior) {\n // A false/null prop cannot claim and remove an attribute owned by other code.\n if (requested !== null) {\n const restore = seed ? null : previous(name);\n\n if (!seed) {\n write(name, requested);\n }\n\n owned.set(name, {\n requested,\n applied: read(name),\n previous: restore,\n });\n }\n } else if (prior.requested !== requested) {\n if (current === prior.applied) {\n write(name, requested);\n prior.applied = read(name);\n }\n\n prior.requested = requested;\n }\n }\n }\n\n /** Seed from the initial snapshot without writing to the DOM. */\n public sync(\n element: HTMLElement,\n attributes: Values,\n style: Record<string, unknown>,\n seed = false,\n ): void {\n const presence = new Map(\n ['class', 'style'].map((name) => [name, element.hasAttribute(name) ? '' : null]),\n );\n const className = attributes.get('class');\n const classes: Values = new Map((className?.match(/\\S+/g) ?? []).map((name) => [name, '']));\n const styles: Values = new Map(\n Object.entries(style).map(([name, value]) => [\n name.startsWith('--')\n ? name\n : name\n .replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)\n .replace(/^ms-/, '-ms-')\n .replace(/^css-float$/, 'float'),\n value === false || value == null ? null : String(value),\n ]),\n );\n\n // Presence is tracked separately from content so empty manager-created attributes go away.\n for (const name of ['class', 'style']) {\n const value = attributes.get(name);\n\n if (value != null) {\n attributes.set(name, '');\n }\n }\n this.syncValues(\n this.classes,\n classes,\n (name) => (element.classList.contains(name) ? '' : null),\n (name, value) => element.classList.toggle(name, value !== null),\n seed,\n );\n this.syncValues(\n this.styles,\n styles,\n (name) => {\n const value = element.style.getPropertyValue(name);\n const priority = element.style.getPropertyPriority(name);\n\n return value ? `${value}${priority ? ` !${priority}` : ''}` : null;\n },\n (name, value) => {\n const important = /\\s*!important\\s*$/i;\n\n element.style.setProperty(\n name,\n value?.replace(important, '') ?? '',\n important.test(value ?? '') ? 'important' : '',\n );\n },\n seed,\n );\n this.syncValues(\n this.attributes,\n attributes,\n (name) => {\n const value = element.getAttribute(name);\n\n return ['class', 'style'].includes(name) && value !== null ? '' : value;\n },\n (name, value) => {\n if (['class', 'style'].includes(name) && element.getAttribute(name)) {\n return;\n }\n\n if (value === null) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value);\n }\n },\n seed,\n (name) => (presence.has(name) ? presence.get(name)! : element.getAttribute(name)),\n );\n }\n}\n\nexport default RootAttributes;\n"],"names":["RootAttributes","attributes","Map","classes","styles","syncValues","owned","desired","read","write","seed","previous","name","Set","keys","prior","get","requested","current","has","applied","restore","set","delete","sync","element","style","presence","map","hasAttribute","className","match","Object","entries","value","startsWith","replace","letter","toLowerCase","String","this","classList","contains","toggle","getPropertyValue","priority","getPropertyPriority","important","setProperty","test","getAttribute","includes","removeAttribute","setAttribute"],"mappings":"AAUA,MAAMA,EACIC,WAAwB,IAAIC,IAE5BC,QAAqB,IAAID,IAEzBE,OAAoB,IAAIF,IAGxBG,WACNC,EACAC,EACAC,EACAC,EACAC,EACAC,EAA4CH,GAE5C,IAAK,MAAMI,KAAQ,IAAIC,IAAI,IAAIP,EAAMQ,UAAWP,EAAQO,SAAU,CAChE,MAAMC,EAAQT,EAAMU,IAAIJ,GAClBK,EAAYV,EAAQS,IAAIJ,IAAS,KACjCM,EAAUV,EAAKI,GAErB,GAAKL,EAAQY,IAAIP,IAMV,GAAKG,EAeDA,EAAME,YAAcA,IACzBC,IAAYH,EAAMK,UACpBX,EAAMG,EAAMK,GACZF,EAAMK,QAAUZ,EAAKI,IAGvBG,EAAME,UAAYA,QAnBlB,GAAkB,OAAdA,EAAoB,CACtB,MAAMI,EAAUX,EAAO,KAAOC,EAASC,GAElCF,GACHD,EAAMG,EAAMK,GAGdX,EAAMgB,IAAIV,EAAM,CACdK,YACAG,QAASZ,EAAKI,GACdD,SAAUU,GAEb,OAnBGN,GAASG,IAAYH,EAAMK,SAC7BX,EAAMG,EAAMG,EAAMJ,UAGpBL,EAAMiB,OAAOX,EAwBhB,CACF,CAGMY,KACLC,EACAxB,EACAyB,EACAhB,GAAO,GAEP,MAAMiB,EAAW,IAAIzB,IACnB,CAAC,QAAS,SAAS0B,KAAKhB,GAAS,CAACA,EAAMa,EAAQI,aAAajB,GAAQ,GAAK,SAEtEkB,EAAY7B,EAAWe,IAAI,SAC3Bb,EAAkB,IAAID,KAAK4B,GAAWC,MAAM,SAAW,IAAIH,KAAKhB,GAAS,CAACA,EAAM,OAChFR,EAAiB,IAAIF,IACzB8B,OAAOC,QAAQP,GAAOE,KAAI,EAAEhB,EAAMsB,KAAW,CAC3CtB,EAAKuB,WAAW,MACZvB,EACAA,EACGwB,QAAQ,UAAWC,GAAW,IAAIA,EAAOC,kBACzCF,QAAQ,OAAQ,QAChBA,QAAQ,cAAe,UACpB,IAAVF,GAA4B,MAATA,EAAgB,KAAOK,OAAOL,OAKrD,IAAK,MAAMtB,IAAQ,CAAC,QAAS,SAAU,CAGxB,MAFCX,EAAWe,IAAIJ,IAG3BX,EAAWqB,IAAIV,EAAM,GAExB,CACD4B,KAAKnC,WACHmC,KAAKrC,QACLA,GACCS,GAAUa,EAAQgB,UAAUC,SAAS9B,GAAQ,GAAK,OACnD,CAACA,EAAMsB,IAAUT,EAAQgB,UAAUE,OAAO/B,EAAgB,OAAVsB,IAChDxB,GAEF8B,KAAKnC,WACHmC,KAAKpC,OACLA,GACCQ,IACC,MAAMsB,EAAQT,EAAQC,MAAMkB,iBAAiBhC,GACvCiC,EAAWpB,EAAQC,MAAMoB,oBAAoBlC,GAEnD,OAAOsB,EAAQ,GAAGA,IAAQW,EAAW,KAAKA,IAAa,KAAO,IAAI,IAEpE,CAACjC,EAAMsB,KACL,MAAMa,EAAY,qBAElBtB,EAAQC,MAAMsB,YACZpC,EACAsB,GAAOE,QAAQW,EAAW,KAAO,GACjCA,EAAUE,KAAKf,GAAS,IAAM,YAAc,GAC7C,GAEHxB,GAEF8B,KAAKnC,WACHmC,KAAKvC,WACLA,GACCW,IACC,MAAMsB,EAAQT,EAAQyB,aAAatC,GAEnC,MAAO,CAAC,QAAS,SAASuC,SAASvC,IAAmB,OAAVsB,EAAiB,GAAKA,CAAK,IAEzE,CAACtB,EAAMsB,KACD,CAAC,QAAS,SAASiB,SAASvC,IAASa,EAAQyB,aAAatC,KAIhD,OAAVsB,EACFT,EAAQ2B,gBAAgBxC,GAExBa,EAAQ4B,aAAazC,EAAMsB,GAC5B,GAEHxB,GACCE,GAAUe,EAASR,IAAIP,GAAQe,EAASX,IAAIJ,GAASa,EAAQyB,aAAatC,IAE9E"}
|