'use strict';

const hookable = require('hookable');
const dom = require('@unhead/dom');
const shared = require('@unhead/shared');

const TAG_WEIGHTS = {
  // aliases
  critical: 2,
  high: 9,
  low: 12,
  // tags
  base: -1,
  title: 1,
  meta: 10
};
function tagWeight(tag) {
  if (typeof tag.tagPriority === "number")
    return tag.tagPriority;
  if (tag.tag === "meta") {
    if (tag.props.charset)
      return -2;
    if (tag.props["http-equiv"] === "content-security-policy")
      return 0;
  }
  const key = tag.tagPriority || tag.tag;
  if (key in TAG_WEIGHTS) {
    return TAG_WEIGHTS[key];
  }
  return 10;
}
const SortModifiers = [{ prefix: "before:", offset: -1 }, { prefix: "after:", offset: 1 }];
function SortTagsPlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tags:resolve": (ctx) => {
        const tagPositionForKey = (key) => ctx.tags.find((tag) => tag._d === key)?._p;
        for (const { prefix, offset } of SortModifiers) {
          for (const tag of ctx.tags.filter((tag2) => typeof tag2.tagPriority === "string" && tag2.tagPriority.startsWith(prefix))) {
            const position = tagPositionForKey(
              tag.tagPriority.replace(prefix, "")
            );
            if (typeof position !== "undefined")
              tag._p = position + offset;
          }
        }
        ctx.tags.sort((a, b) => a._p - b._p).sort((a, b) => tagWeight(a) - tagWeight(b));
      }
    }
  });
}

function TitleTemplatePlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tags:resolve": (ctx) => {
        const { tags } = ctx;
        let titleTemplateIdx = tags.findIndex((i) => i.tag === "titleTemplate");
        const titleIdx = tags.findIndex((i) => i.tag === "title");
        if (titleIdx !== -1 && titleTemplateIdx !== -1) {
          const newTitle = shared.resolveTitleTemplate(
            tags[titleTemplateIdx].textContent,
            tags[titleIdx].textContent
          );
          if (newTitle !== null) {
            tags[titleIdx].textContent = newTitle || tags[titleIdx].textContent;
          } else {
            delete tags[titleIdx];
          }
        } else if (titleTemplateIdx !== -1) {
          const newTitle = shared.resolveTitleTemplate(
            tags[titleTemplateIdx].textContent
          );
          if (newTitle !== null) {
            tags[titleTemplateIdx].textContent = newTitle;
            tags[titleTemplateIdx].tag = "title";
            titleTemplateIdx = -1;
          }
        }
        if (titleTemplateIdx !== -1) {
          delete tags[titleTemplateIdx];
        }
        ctx.tags = tags.filter(Boolean);
      }
    }
  });
}

function DeprecatedTagAttrPlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tag:normalise": function({ tag }) {
        if (typeof tag.props.body !== "undefined") {
          tag.tagPosition = "bodyClose";
          delete tag.props.body;
        }
      }
    }
  });
}

const DupeableTags = ["link", "style", "script", "noscript"];
function ProvideTagHashPlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tag:normalise": ({ tag, resolvedOptions }) => {
        if (resolvedOptions.experimentalHashHydration === true) {
          tag._h = shared.hashTag(tag);
        }
        if (tag.key && DupeableTags.includes(tag.tag)) {
          tag._h = shared.hashCode(tag.key);
          tag.props[`data-h-${tag._h}`] = "";
        }
      }
    }
  });
}

const ValidEventTags = ["script", "link", "bodyAttrs"];
function EventHandlersPlugin() {
  const stripEventHandlers = (mode, tag) => {
    const props = {};
    const eventHandlers = {};
    Object.entries(tag.props).forEach(([key, value]) => {
      if (key.startsWith("on") && typeof value === "function")
        eventHandlers[key] = value;
      else
        props[key] = value;
    });
    let delayedSrc;
    if (mode === "dom" && tag.tag === "script" && typeof props.src === "string" && typeof eventHandlers.onload !== "undefined") {
      delayedSrc = props.src;
      delete props.src;
    }
    return { props, eventHandlers, delayedSrc };
  };
  return shared.defineHeadPlugin({
    hooks: {
      "ssr:render": function(ctx) {
        ctx.tags = ctx.tags.map((tag) => {
          if (!ValidEventTags.includes(tag.tag))
            return tag;
          if (!Object.entries(tag.props).find(([key, value]) => key.startsWith("on") && typeof value === "function"))
            return tag;
          tag.props = stripEventHandlers("ssr", tag).props;
          return tag;
        });
      },
      "dom:beforeRenderTag": function(ctx) {
        if (!ValidEventTags.includes(ctx.tag.tag))
          return;
        if (!Object.entries(ctx.tag.props).find(([key, value]) => key.startsWith("on") && typeof value === "function"))
          return;
        const { props, eventHandlers, delayedSrc } = stripEventHandlers("dom", ctx.tag);
        if (!Object.keys(eventHandlers).length)
          return;
        ctx.tag.props = props;
        ctx.tag._eventHandlers = eventHandlers;
        ctx.tag._delayedSrc = delayedSrc;
      },
      "dom:renderTag": function(ctx) {
        const $el = ctx.$el;
        if (!ctx.tag._eventHandlers || !$el)
          return;
        const $eventListenerTarget = ctx.tag.tag === "bodyAttrs" && typeof window !== "undefined" ? window : $el;
        Object.entries(ctx.tag._eventHandlers).forEach(([k, value]) => {
          const sdeKey = `${ctx.tag._d || ctx.tag._p}:${k}`;
          const eventName = k.slice(2).toLowerCase();
          const eventDedupeKey = `data-h-${eventName}`;
          ctx.markSideEffect(sdeKey, () => {
          });
          if ($el.hasAttribute(eventDedupeKey))
            return;
          const handler = value;
          $el.setAttribute(eventDedupeKey, "");
          $eventListenerTarget.addEventListener(eventName, handler);
          if (ctx.entry) {
            ctx.entry._sde[sdeKey] = () => {
              $eventListenerTarget.removeEventListener(eventName, handler);
              $el.removeAttribute(eventDedupeKey);
            };
          }
        });
        if (ctx.tag._delayedSrc) {
          $el.setAttribute("src", ctx.tag._delayedSrc);
        }
      }
    }
  });
}

const UsesMergeStrategy = ["templateParams", "htmlAttrs", "bodyAttrs"];
function DedupesTagsPlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tag:normalise": function({ tag }) {
        ["hid", "vmid", "key"].forEach((key) => {
          if (tag.props[key]) {
            tag.key = tag.props[key];
            delete tag.props[key];
          }
        });
        const generatedKey = shared.tagDedupeKey(tag);
        const dedupe = generatedKey || (tag.key ? `${tag.tag}:${tag.key}` : false);
        if (dedupe)
          tag._d = dedupe;
      },
      "tags:resolve": function(ctx) {
        const deduping = {};
        ctx.tags.forEach((tag) => {
          const dedupeKey = (tag.key ? `${tag.tag}:${tag.key}` : tag._d) || tag._p;
          const dupedTag = deduping[dedupeKey];
          if (dupedTag) {
            let strategy = tag?.tagDuplicateStrategy;
            if (!strategy && UsesMergeStrategy.includes(tag.tag))
              strategy = "merge";
            if (strategy === "merge") {
              const oldProps = dupedTag.props;
              ["class", "style"].forEach((key) => {
                if (tag.props[key] && oldProps[key]) {
                  if (key === "style" && !oldProps[key].endsWith(";"))
                    oldProps[key] += ";";
                  tag.props[key] = `${oldProps[key]} ${tag.props[key]}`;
                }
              });
              deduping[dedupeKey].props = {
                ...oldProps,
                ...tag.props
              };
              return;
            } else if (tag._e === dupedTag._e) {
              dupedTag._duped = dupedTag._duped || [];
              tag._d = `${dupedTag._d}:${dupedTag._duped.length + 1}`;
              dupedTag._duped.push(tag);
              return;
            }
          }
          const propCount = Object.keys(tag.props).length + (tag.innerHTML ? 1 : 0) + (tag.textContent ? 1 : 0);
          if (shared.HasElementTags.includes(tag.tag) && propCount === 0) {
            delete deduping[dedupeKey];
            return;
          }
          deduping[dedupeKey] = tag;
        });
        const newTags = [];
        Object.values(deduping).forEach((tag) => {
          const dupes = tag._duped;
          delete tag._duped;
          newTags.push(tag);
          if (dupes)
            newTags.push(...dupes);
        });
        ctx.tags = newTags;
      }
    }
  });
}

function processTemplateParams(s, config) {
  function sub(token) {
    if (["s", "pageTitle"].includes(token))
      return config.pageTitle;
    let val;
    if (token.includes(".")) {
      val = token.split(".").reduce((acc, key) => acc ? acc[key] || void 0 : void 0, config);
    } else {
      val = config[token];
    }
    return typeof val !== "undefined" ? val || "" : false;
  }
  let decoded = s;
  try {
    decoded = decodeURI(s);
  } catch {
  }
  const tokens = (decoded.match(/%(\w+\.+\w+)|%(\w+)/g) || []).sort().reverse();
  tokens.forEach((token) => {
    const re = sub(token.slice(1));
    if (typeof re === "string") {
      s = s.replace(new RegExp(`\\${token}(\\W|$)`, "g"), `${re}$1`).trim();
    }
  });
  if (config.separator) {
    if (s.endsWith(config.separator))
      s = s.slice(0, -config.separator.length).trim();
    if (s.startsWith(config.separator))
      s = s.slice(config.separator.length).trim();
    s = s.replace(new RegExp(`\\${config.separator}\\s*\\${config.separator}`, "g"), config.separator);
  }
  return s;
}
function TemplateParamsPlugin() {
  return shared.defineHeadPlugin({
    hooks: {
      "tags:resolve": (ctx) => {
        const { tags } = ctx;
        const title = tags.find((tag) => tag.tag === "title")?.textContent;
        const idx = tags.findIndex((tag) => tag.tag === "templateParams");
        const params = idx !== -1 ? tags[idx].props : {};
        params.pageTitle = params.pageTitle || title || "";
        for (const tag of tags) {
          if (["titleTemplate", "title"].includes(tag.tag) && typeof tag.textContent === "string") {
            tag.textContent = processTemplateParams(tag.textContent, params);
          } else if (tag.tag === "meta" && typeof tag.props.content === "string") {
            tag.props.content = processTemplateParams(tag.props.content, params);
          } else if (tag.tag === "link" && typeof tag.props.href === "string") {
            tag.props.href = processTemplateParams(tag.props.href, params);
          } else if (tag.tag === "script" && ["application/json", "application/ld+json"].includes(tag.props.type) && typeof tag.innerHTML === "string") {
            try {
              tag.innerHTML = JSON.stringify(JSON.parse(tag.innerHTML), (key, val) => {
                if (typeof val === "string")
                  return processTemplateParams(val, params);
                return val;
              });
            } catch {
            }
          }
        }
        ctx.tags = tags.filter((tag) => tag.tag !== "templateParams");
      }
    }
  });
}

const IsBrowser = typeof window !== "undefined";

exports.activeHead = void 0;
function setActiveHead(head) {
  return exports.activeHead = head;
}
function getActiveHead() {
  return exports.activeHead;
}

function useHead(input, options = {}) {
  const head = getActiveHead();
  if (head) {
    const isBrowser = IsBrowser || head.resolvedOptions?.document;
    if (options.mode === "server" && isBrowser || options.mode === "client" && !isBrowser)
      return;
    return head.push(input, options);
  }
}

function useHeadSafe(input, options = {}) {
  return useHead(input, {
    ...options || {},
    transform: whitelistSafeInput
  });
}

function useServerHead(input, options = {}) {
  return useHead(input, { ...options, mode: "server" });
}

function useServerHeadSafe(input, options = {}) {
  return useHeadSafe(input, { ...options, mode: "server" });
}

function useSeoMeta(input, options) {
  const { title, titleTemplate, ...meta } = input;
  return useHead({
    title,
    titleTemplate,
    meta: unpackMeta(meta)
  }, options);
}

function useServerSeoMeta(input, options) {
  return useSeoMeta(input, {
    ...options || {},
    mode: "server"
  });
}

function useTagTitle(title) {
  return useHead({ title });
}
function useTagBase(base) {
  return useHead({ base });
}
function useTagMeta(meta) {
  return useHead({ meta: shared.asArray(meta) });
}
function useTagMetaFlat(meta) {
  return useTagMeta(unpackMeta(meta));
}
function useTagLink(link) {
  return useHead({ link: shared.asArray(link) });
}
function useTagScript(script) {
  return useHead({ script: shared.asArray(script) });
}
function useTagStyle(style) {
  return useHead({ style: shared.asArray(style) });
}
function useTagNoscript(noscript) {
  return useHead({ noscript: shared.asArray(noscript) });
}
function useHtmlAttrs(attrs) {
  return useHead({ htmlAttrs: attrs });
}
function useBodyAttrs(attrs) {
  return useHead({ bodyAttrs: attrs });
}
function useTitleTemplate(titleTemplate) {
  return useHead({ titleTemplate });
}
function useServerTagTitle(title) {
  return useServerHead({ title });
}
function useServerTagBase(base) {
  return useServerHead({ base });
}
function useServerTagMeta(meta) {
  return useServerHead({ meta: shared.asArray(meta) });
}
function useServerTagMetaFlat(meta) {
  return useServerTagMeta(unpackMeta(meta));
}
function useServerTagLink(link) {
  return useServerHead({ link: shared.asArray(link) });
}
function useServerTagScript(script) {
  return useServerHead({ script: shared.asArray(script) });
}
function useServerTagStyle(style) {
  return useServerHead({ style: shared.asArray(style) });
}
function useServerTagNoscript(noscript) {
  return useServerHead({ noscript: shared.asArray(noscript) });
}
function useServerHtmlAttrs(attrs) {
  return useServerHead({ htmlAttrs: attrs });
}
function useServerBodyAttrs(attrs) {
  return useServerHead({ bodyAttrs: attrs });
}
function useServerTitleTemplate(titleTemplate) {
  return useServerHead({ titleTemplate });
}

function asArray(input) {
  return Array.isArray(input) ? input : [input];
}
const InternalKeySymbol = "_$key";
function packObject(input, options) {
  const keys = Object.keys(input);
  let [k, v] = keys;
  options = options || {};
  options.key = options.key || k;
  options.value = options.value || v;
  options.resolveKey = options.resolveKey || ((k2) => k2);
  const resolveKey = (index) => {
    const arr = asArray(options?.[index]);
    return arr.find((k2) => {
      if (typeof k2 === "string" && k2.includes(".")) {
        return k2;
      }
      return k2 && keys.includes(k2);
    });
  };
  const resolveValue = (k2, input2) => {
    if (k2.includes(".")) {
      const paths = k2.split(".");
      let val = input2;
      for (const path of paths)
        val = val[path];
      return val;
    }
    return input2[k2];
  };
  k = resolveKey("key") || k;
  v = resolveKey("value") || v;
  const dedupeKeyPrefix = input.key ? `${InternalKeySymbol}${input.key}-` : "";
  let keyValue = resolveValue(k, input);
  keyValue = options.resolveKey(keyValue);
  return {
    [`${dedupeKeyPrefix}${keyValue}`]: resolveValue(v, input)
  };
}

function packArray(input, options) {
  const packed = {};
  for (const i of input) {
    const packedObj = packObject(i, options);
    const pKey = Object.keys(packedObj)[0];
    const isDedupeKey = pKey.startsWith(InternalKeySymbol);
    if (!isDedupeKey && packed[pKey]) {
      packed[pKey] = Array.isArray(packed[pKey]) ? packed[pKey] : [packed[pKey]];
      packed[pKey].push(Object.values(packedObj)[0]);
    } else {
      packed[isDedupeKey ? pKey.split("-").slice(1).join("-") || pKey : pKey] = packedObj[pKey];
    }
  }
  return packed;
}

function unpackToArray(input, options) {
  const unpacked = [];
  const kFn = options.resolveKeyData || ((ctx) => ctx.key);
  const vFn = options.resolveValueData || ((ctx) => ctx.value);
  for (const [k, v] of Object.entries(input)) {
    unpacked.push(...(Array.isArray(v) ? v : [v]).map((i) => {
      const ctx = { key: k, value: i };
      const val = vFn(ctx);
      if (typeof val === "object")
        return unpackToArray(val, options);
      if (Array.isArray(val))
        return val;
      return {
        [typeof options.key === "function" ? options.key(ctx) : options.key]: kFn(ctx),
        [typeof options.value === "function" ? options.value(ctx) : options.value]: val
      };
    }).flat());
  }
  return unpacked;
}

function unpackToString(value, options) {
  return Object.entries(value).map(([key, value2]) => {
    if (typeof value2 === "object")
      value2 = unpackToString(value2, options);
    if (options.resolve) {
      const resolved = options.resolve({ key, value: value2 });
      if (resolved)
        return resolved;
    }
    if (typeof value2 === "number")
      value2 = value2.toString();
    if (typeof value2 === "string" && options.wrapValue) {
      value2 = value2.replace(new RegExp(options.wrapValue, "g"), `\\${options.wrapValue}`);
      value2 = `${options.wrapValue}${value2}${options.wrapValue}`;
    }
    return `${key}${options.keyValueSeparator || ""}${value2}`;
  }).join(options.entrySeparator || "");
}

const MetaPackingSchema = {
  robots: {
    unpack: {
      keyValueSeparator: ":"
    }
  },
  // Pragma directives
  contentSecurityPolicy: {
    unpack: {
      keyValueSeparator: " ",
      entrySeparator: "; "
    },
    metaKey: "http-equiv"
  },
  fbAppId: {
    keyValue: "fb:app_id",
    metaKey: "property"
  },
  ogSiteName: {
    keyValue: "og:site_name"
  },
  msapplicationTileImage: {
    keyValue: "msapplication-TileImage"
  },
  /**
   * Tile colour for windows
   */
  msapplicationTileColor: {
    keyValue: "msapplication-TileColor"
  },
  /**
   * URL of a config for windows tile.
   */
  msapplicationConfig: {
    keyValue: "msapplication-Config"
  },
  charset: {
    metaKey: "charset"
  },
  contentType: {
    metaKey: "http-equiv"
  },
  defaultStyle: {
    metaKey: "http-equiv"
  },
  xUaCompatible: {
    metaKey: "http-equiv"
  },
  refresh: {
    metaKey: "http-equiv"
  }
};

function packMeta(inputs) {
  const mappedPackingSchema = Object.entries(MetaPackingSchema).map(([key, value]) => [key, value.keyValue]);
  return packArray(inputs, {
    key: ["name", "property", "httpEquiv", "http-equiv", "charset"],
    value: ["content", "charset"],
    resolveKey(k) {
      let key = mappedPackingSchema.filter((sk) => sk[1] === k)?.[0]?.[0] || k;
      const replacer = (_, letter) => letter?.toUpperCase();
      key = key.replace(/:([a-z])/g, replacer).replace(/-([a-z])/g, replacer);
      return key;
    }
  });
}

const ArrayableInputs = ["Image", "Video", "Audio"];
const ColonPrefixKeys = /^(og|twitter|fb)/;
const PropertyPrefixKeys = /^(og|fb)/;
function resolveMetaKeyType(key) {
  return PropertyPrefixKeys.test(key) ? "property" : MetaPackingSchema[key]?.metaKey || "name";
}
function resolveMetaKeyValue(key) {
  return MetaPackingSchema[key]?.keyValue || fixKeyCase(key);
}
function fixKeyCase(key) {
  key = key.replace(/([A-Z])/g, "-$1").toLowerCase();
  if (ColonPrefixKeys.test(key)) {
    key = key.replace("secure-url", "secure_url").replace(/-/g, ":");
  }
  return key;
}
function changeKeyCasingDeep(input) {
  if (Array.isArray(input)) {
    return input.map((entry) => changeKeyCasingDeep(entry));
  }
  if (typeof input !== "object" || Array.isArray(input))
    return input;
  const output = {};
  for (const [key, value] of Object.entries(input))
    output[fixKeyCase(key)] = changeKeyCasingDeep(value);
  return output;
}
function unpackMeta(input) {
  const extras = [];
  ArrayableInputs.forEach((key) => {
    const ogKey = `og:${key.toLowerCase()}`;
    const inputKey = `og${key}`;
    const val = input[inputKey];
    if (typeof val === "object") {
      (Array.isArray(val) ? val : [val]).forEach((entry) => {
        if (!entry)
          return;
        const unpackedEntry = unpackToArray(entry, {
          key: "property",
          value: "content",
          resolveKeyData({ key: key2 }) {
            return fixKeyCase(`${ogKey}${key2 !== "url" ? `:${key2}` : ""}`);
          },
          resolveValueData({ value }) {
            return typeof value === "number" ? value.toString() : value;
          }
        });
        extras.push(
          ...unpackedEntry.sort((a, b) => a.property === ogKey ? -1 : b.property === ogKey ? 1 : 0)
        );
      });
      delete input[inputKey];
    }
  });
  const meta = unpackToArray(input, {
    key({ key }) {
      return resolveMetaKeyType(key);
    },
    value({ key }) {
      return key === "charset" ? "charset" : "content";
    },
    resolveKeyData({ key }) {
      return resolveMetaKeyValue(key);
    },
    resolveValueData({ value, key }) {
      if (value === null)
        return "_null";
      if (typeof value === "object")
        return resolvePackedMetaObjectValue(value, key);
      return typeof value === "number" ? value.toString() : value;
    }
  });
  return [...extras, ...meta].filter((v) => typeof v.content === "undefined" || v.content !== "_null");
}
function resolvePackedMetaObjectValue(value, key) {
  const definition = MetaPackingSchema[key];
  if (key === "refresh")
    return `${value.seconds};url=${value.url}`;
  return unpackToString(
    changeKeyCasingDeep(value),
    {
      entrySeparator: ", ",
      keyValueSeparator: "=",
      resolve({ value: value2, key: key2 }) {
        if (value2 === null)
          return "";
        if (typeof value2 === "boolean")
          return `${key2}`;
      },
      ...definition?.unpack
    }
  );
}

async function normaliseTag(tagName, input) {
  const tag = { tag: tagName, props: {} };
  if (tagName === "templateParams") {
    tag.props = input;
    return tag;
  }
  if (["title", "titleTemplate"].includes(tagName)) {
    tag.textContent = input instanceof Promise ? await input : input;
    return tag;
  }
  if (typeof input === "string") {
    if (!["script", "noscript", "style"].includes(tagName))
      return false;
    if (tagName === "script" && (/^(https?:)?\/\//.test(input) || input.startsWith("/")))
      tag.props.src = input;
    else
      tag.innerHTML = input;
    return tag;
  }
  tag.props = await normaliseProps(tagName, { ...input });
  if (tag.props.children) {
    tag.props.innerHTML = tag.props.children;
  }
  delete tag.props.children;
  Object.keys(tag.props).filter((k) => shared.TagConfigKeys.includes(k)).forEach((k) => {
    if (!["innerHTML", "textContent"].includes(k) || shared.TagsWithInnerContent.includes(tag.tag)) {
      tag[k] = tag.props[k];
    }
    delete tag.props[k];
  });
  ["innerHTML", "textContent"].forEach((k) => {
    if (tag.tag === "script" && typeof tag[k] === "string" && ["application/ld+json", "application/json"].includes(tag.props.type)) {
      try {
        tag[k] = JSON.parse(tag[k]);
      } catch (e) {
        tag[k] = "";
      }
    }
    if (typeof tag[k] === "object")
      tag[k] = JSON.stringify(tag[k]);
  });
  if (tag.props.class)
    tag.props.class = normaliseClassProp(tag.props.class);
  if (tag.props.content && Array.isArray(tag.props.content))
    return tag.props.content.map((v) => ({ ...tag, props: { ...tag.props, content: v } }));
  return tag;
}
function normaliseClassProp(v) {
  if (typeof v === "object" && !Array.isArray(v)) {
    v = Object.keys(v).filter((k) => v[k]);
  }
  return (Array.isArray(v) ? v.join(" ") : v).split(" ").filter((c) => c.trim()).filter(Boolean).join(" ");
}
async function normaliseProps(tagName, props) {
  for (const k of Object.keys(props)) {
    const isDataKey = k.startsWith("data-");
    if (props[k] instanceof Promise) {
      props[k] = await props[k];
    }
    if (String(props[k]) === "true") {
      props[k] = isDataKey ? "true" : "";
    } else if (String(props[k]) === "false") {
      if (isDataKey) {
        props[k] = "false";
      } else {
        delete props[k];
      }
    }
  }
  return props;
}
const TagEntityBits = 10;
async function normaliseEntryTags(e) {
  const tagPromises = [];
  Object.entries(e.resolvedInput).filter(([k, v]) => typeof v !== "undefined" && shared.ValidHeadTags.includes(k)).forEach(([k, value]) => {
    const v = shared.asArray(value);
    tagPromises.push(...v.map((props) => normaliseTag(k, props)).flat());
  });
  return (await Promise.all(tagPromises)).flat().filter(Boolean).map((t, i) => {
    t._e = e._i;
    t._p = (e._i << TagEntityBits) + i;
    return t;
  });
}

const WhitelistAttributes = {
  htmlAttrs: ["id", "class", "lang", "dir"],
  bodyAttrs: ["id", "class"],
  meta: ["id", "name", "property", "charset", "content"],
  noscript: ["id", "textContent"],
  script: ["id", "type", "textContent"],
  link: ["id", "color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"]
};
function whitelistSafeInput(input) {
  const filtered = {};
  Object.keys(input).forEach((key) => {
    const tagValue = input[key];
    if (!tagValue)
      return;
    switch (key) {
      case "title":
      case "titleTemplate":
      case "templateParams":
        filtered[key] = tagValue;
        break;
      case "htmlAttrs":
      case "bodyAttrs":
        filtered[key] = {};
        WhitelistAttributes[key].forEach((a) => {
          if (tagValue[a])
            filtered[key][a] = tagValue[a];
        });
        Object.keys(tagValue || {}).filter((a) => a.startsWith("data-")).forEach((a) => {
          filtered[key][a] = tagValue[a];
        });
        break;
      case "meta":
        if (Array.isArray(tagValue)) {
          filtered[key] = tagValue.map((meta) => {
            const safeMeta = {};
            WhitelistAttributes.meta.forEach((key2) => {
              if (meta[key2] || key2.startsWith("data-"))
                safeMeta[key2] = meta[key2];
            });
            return safeMeta;
          }).filter((meta) => Object.keys(meta).length > 0);
        }
        break;
      case "link":
        if (Array.isArray(tagValue)) {
          filtered[key] = tagValue.map((meta) => {
            const link = {};
            WhitelistAttributes.link.forEach((key2) => {
              if (key2 === "rel" && ["stylesheet", "canonical", "modulepreload", "prerender", "preload", "prefetch"].includes(meta[key2]))
                return;
              if (key2 === "href") {
                try {
                  const url = new URL(meta[key2]);
                  if (["javascript:", "data:"].includes(url.protocol))
                    return;
                  link[key2] = meta[key2];
                } catch (e) {
                }
              } else if (meta[key2] || key2.startsWith("data-")) {
                link[key2] = meta[key2];
              }
            });
            return link;
          }).filter((link) => Object.keys(link).length > 1 && !!link.rel);
        }
        break;
      case "noscript":
        if (Array.isArray(tagValue)) {
          filtered[key] = tagValue.map((meta) => {
            const noscript = {};
            WhitelistAttributes.noscript.forEach((key2) => {
              if (meta[key2] || key2.startsWith("data-"))
                noscript[key2] = meta[key2];
            });
            return noscript;
          }).filter((meta) => Object.keys(meta).length > 0);
        }
        break;
      case "script":
        if (Array.isArray(tagValue)) {
          filtered[key] = tagValue.map((script) => {
            const safeScript = {};
            WhitelistAttributes.script.forEach((s) => {
              if (script[s] || s.startsWith("data-")) {
                if (s === "textContent") {
                  try {
                    const jsonVal = typeof script[s] === "string" ? JSON.parse(script[s]) : script[s];
                    safeScript[s] = JSON.stringify(jsonVal, null, 0);
                  } catch (e) {
                  }
                } else {
                  safeScript[s] = script[s];
                }
              }
            });
            return safeScript;
          }).filter((meta) => Object.keys(meta).length > 0);
        }
        break;
    }
  });
  return filtered;
}

function CorePlugins() {
  return [
    // dedupe needs to come first
    DedupesTagsPlugin(),
    SortTagsPlugin(),
    TemplateParamsPlugin(),
    TitleTemplatePlugin(),
    ProvideTagHashPlugin(),
    EventHandlersPlugin(),
    DeprecatedTagAttrPlugin()
  ];
}
function DOMPlugins(options = {}) {
  return [
    dom.PatchDomOnEntryUpdatesPlugin({ document: options?.document, delayFn: options?.domDelayFn })
  ];
}
function createHead(options = {}) {
  const head = createHeadCore({
    ...options,
    plugins: [...DOMPlugins(options), ...options?.plugins || []]
  });
  if (options.experimentalHashHydration && head.resolvedOptions.document)
    head._hash = dom.maybeGetSSRHash(head.resolvedOptions.document);
  setActiveHead(head);
  return head;
}
function createServerHead(options = {}) {
  const head = createHeadCore(options);
  setActiveHead(head);
  return head;
}
function createHeadCore(options = {}) {
  let entries = [];
  let _sde = {};
  let _eid = 0;
  const hooks = hookable.createHooks();
  if (options?.hooks)
    hooks.addHooks(options.hooks);
  options.plugins = [
    ...CorePlugins(),
    ...options?.plugins || []
  ];
  options.plugins.forEach((p) => p.hooks && hooks.addHooks(p.hooks));
  options.document = options.document || (IsBrowser ? document : void 0);
  const updated = () => hooks.callHook("entries:updated", head);
  const head = {
    resolvedOptions: options,
    headEntries() {
      return entries;
    },
    get hooks() {
      return hooks;
    },
    use(plugin) {
      if (plugin.hooks)
        hooks.addHooks(plugin.hooks);
    },
    push(input, options2) {
      const activeEntry = {
        _i: _eid++,
        input,
        _sde: {}
      };
      if (options2?.mode)
        activeEntry._m = options2?.mode;
      if (options2?.transform) {
        activeEntry._t = options2?.transform;
      }
      entries.push(activeEntry);
      updated();
      return {
        dispose() {
          entries = entries.filter((e) => {
            if (e._i !== activeEntry._i)
              return true;
            _sde = { ..._sde, ...e._sde || {} };
            e._sde = {};
            updated();
            return false;
          });
        },
        // a patch is the same as creating a new entry, just a nice DX
        patch(input2) {
          entries = entries.map((e) => {
            if (e._i === activeEntry._i) {
              activeEntry.input = e.input = input2;
              updated();
            }
            return e;
          });
        }
      };
    },
    async resolveTags() {
      const resolveCtx = { tags: [], entries: [...entries] };
      await hooks.callHook("entries:resolve", resolveCtx);
      for (const entry of resolveCtx.entries) {
        const transformer = entry._t || ((i) => i);
        entry.resolvedInput = transformer(entry.resolvedInput || entry.input);
        if (entry.resolvedInput) {
          for (const tag of await normaliseEntryTags(entry)) {
            const tagCtx = { tag, entry, resolvedOptions: head.resolvedOptions };
            await hooks.callHook("tag:normalise", tagCtx);
            resolveCtx.tags.push(tagCtx.tag);
          }
        }
      }
      await hooks.callHook("tags:resolve", resolveCtx);
      return resolveCtx.tags;
    },
    _popSideEffectQueue() {
      const sde = { ..._sde };
      _sde = {};
      return sde;
    },
    _elMap: {}
  };
  head.hooks.callHook("init", head);
  return head;
}

const coreComposableNames = [
  "getActiveHead"
];
const composableNames = [
  "useHead",
  "useSeoMeta",
  "useHeadSafe",
  "useServerHead",
  "useServerSeoMeta",
  "useServerHeadSafe",
  // deprecated
  "useTagTitle",
  "useTagBase",
  "useTagMeta",
  "useTagMetaFlat",
  "useTagLink",
  "useTagScript",
  "useTagStyle",
  "useTagNoscript",
  "useHtmlAttrs",
  "useBodyAttrs",
  "useTitleTemplate",
  "useServerTagTitle",
  "useServerTagBase",
  "useServerTagMeta",
  "useServerTagMetaFlat",
  "useServerTagLink",
  "useServerTagScript",
  "useServerTagStyle",
  "useServerTagNoscript",
  "useServerHtmlAttrs",
  "useServerBodyAttrs",
  "useServerTitleTemplate"
];
const unheadComposablesImports = [
  {
    from: "unhead",
    imports: [...coreComposableNames, ...composableNames]
  }
];

exports.CorePlugins = CorePlugins;
exports.DOMPlugins = DOMPlugins;
exports.DedupesTagsPlugin = DedupesTagsPlugin;
exports.DeprecatedTagAttrPlugin = DeprecatedTagAttrPlugin;
exports.EventHandlersPlugin = EventHandlersPlugin;
exports.ProvideTagHashPlugin = ProvideTagHashPlugin;
exports.SortModifiers = SortModifiers;
exports.SortTagsPlugin = SortTagsPlugin;
exports.TAG_WEIGHTS = TAG_WEIGHTS;
exports.TagEntityBits = TagEntityBits;
exports.TemplateParamsPlugin = TemplateParamsPlugin;
exports.TitleTemplatePlugin = TitleTemplatePlugin;
exports.composableNames = composableNames;
exports.createHead = createHead;
exports.createHeadCore = createHeadCore;
exports.createServerHead = createServerHead;
exports.getActiveHead = getActiveHead;
exports.normaliseClassProp = normaliseClassProp;
exports.normaliseEntryTags = normaliseEntryTags;
exports.normaliseProps = normaliseProps;
exports.normaliseTag = normaliseTag;
exports.packMeta = packMeta;
exports.processTemplateParams = processTemplateParams;
exports.resolveMetaKeyType = resolveMetaKeyType;
exports.resolveMetaKeyValue = resolveMetaKeyValue;
exports.resolvePackedMetaObjectValue = resolvePackedMetaObjectValue;
exports.setActiveHead = setActiveHead;
exports.tagWeight = tagWeight;
exports.unheadComposablesImports = unheadComposablesImports;
exports.unpackMeta = unpackMeta;
exports.useBodyAttrs = useBodyAttrs;
exports.useHead = useHead;
exports.useHeadSafe = useHeadSafe;
exports.useHtmlAttrs = useHtmlAttrs;
exports.useSeoMeta = useSeoMeta;
exports.useServerBodyAttrs = useServerBodyAttrs;
exports.useServerHead = useServerHead;
exports.useServerHeadSafe = useServerHeadSafe;
exports.useServerHtmlAttrs = useServerHtmlAttrs;
exports.useServerSeoMeta = useServerSeoMeta;
exports.useServerTagBase = useServerTagBase;
exports.useServerTagLink = useServerTagLink;
exports.useServerTagMeta = useServerTagMeta;
exports.useServerTagMetaFlat = useServerTagMetaFlat;
exports.useServerTagNoscript = useServerTagNoscript;
exports.useServerTagScript = useServerTagScript;
exports.useServerTagStyle = useServerTagStyle;
exports.useServerTagTitle = useServerTagTitle;
exports.useServerTitleTemplate = useServerTitleTemplate;
exports.useTagBase = useTagBase;
exports.useTagLink = useTagLink;
exports.useTagMeta = useTagMeta;
exports.useTagMetaFlat = useTagMetaFlat;
exports.useTagNoscript = useTagNoscript;
exports.useTagScript = useTagScript;
exports.useTagStyle = useTagStyle;
exports.useTagTitle = useTagTitle;
exports.useTitleTemplate = useTitleTemplate;
exports.whitelistSafeInput = whitelistSafeInput;
