'use strict';

var index$1 = require('./index-BjHIBY-I.js');
var markdownParser = require('./markdown-parser-DCnW9yak.js');
var config = require('./config-dit--4m5.js');
var index = require('./index-mbfaT7TB.js');
require('./_commonjsHelpers-BJu3ubxk.js');

class ImageIntersectionObserver {
    /**
     * @param containerElement - The element containing images to observe.
     */
    constructor(containerElement) {
        this.handleIntersection = (entries) => {
            for (const entry of entries) {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    const dataSrc = img.dataset.src;
                    if (dataSrc) {
                        img.setAttribute('src', dataSrc);
                        delete img.dataset.src;
                    }
                    this.observer.unobserve(img);
                }
            }
        };
        this.observer = new IntersectionObserver(this.handleIntersection);
        const images = containerElement.querySelectorAll('img');
        for (const img of images) {
            this.observer.observe(img);
        }
    }
    disconnect() {
        this.observer.disconnect();
    }
}

var _a;
/**
 * Map of URL-bearing property names to their allowed protocols, sourced
 * from rehype-sanitize's defaultSchema. This means protocol updates from
 * Dependabot bumps to rehype-sanitize automatically apply here too —
 * no custom blocklist to maintain.
 *
 * We can't use rehype-sanitize directly for URL sanitization because it
 * operates on HTML attributes, not on values inside JSON strings. By the
 * time rehype-sanitize runs, `link` is just a raw JSON string attribute —
 * the `href` only becomes visible after JSON parsing in hydration.
 * So we replicate rehype-sanitize's protocol validation logic here,
 * using its own protocol list as the source of truth.
 *
 * The map covers all URL-bearing property names that rehype-sanitize
 * defines protocols for: `href`, `src`, `cite`, and `longDesc`.
 *
 * @internal
 */
const SAFE_PROTOCOLS_BY_PROPERTY = new Map(Object.entries((_a = index.defaultSchema.protocols) !== null && _a !== void 0 ? _a : {}).map(([prop, protocols]) => [
    prop,
    new Set(protocols),
]));

/**
 * After innerHTML is set on a container, custom elements receive all
 * attribute values as strings. This function walks whitelisted custom
 * elements and parses any attribute values that look like JSON objects
 * or arrays, setting them as JS properties instead.
 *
 * This enables markdown content to include custom elements with complex
 * props, e.g.:
 * ```html
 * <limel-chip text="GitHub" link='{"href":"https://github.com","target":"_blank"}'></limel-chip>
 * ```
 *
 * @param container - The root element to search within.
 * @param whitelist - The list of whitelisted custom element definitions.
 */
function hydrateCustomElements(container, whitelist) {
    if (!container || !(whitelist === null || whitelist === void 0 ? void 0 : whitelist.length)) {
        return;
    }
    for (const definition of whitelist) {
        const elements = container.querySelectorAll(definition.tagName);
        for (const element of elements) {
            hydrateElement(element, definition.attributes);
        }
    }
}
function hydrateElement(element, attributes) {
    for (const attrName of attributes) {
        const value = element.getAttribute(attrName);
        if (!value) {
            continue;
        }
        const parsed = tryParseJson(value);
        if (parsed !== undefined) {
            const sanitized = sanitizeUrls(parsed);
            // Set the JS property (camelCase) instead of the attribute
            const propName = attributeToPropName(attrName);
            element[propName] = sanitized;
        }
    }
}
function tryParseJson(value) {
    const trimmed = value.trim();
    if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
        (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
        try {
            return JSON.parse(trimmed);
        }
        catch (_a) {
            // The sanitizer may HTML-encode quotes inside attribute values.
            // Try decoding common HTML entities before giving up.
            try {
                const decoded = trimmed
                    .replaceAll('&#x22;', '"')
                    .replaceAll('&#34;', '"')
                    .replaceAll('&quot;', '"')
                    .replaceAll('&#x27;', "'")
                    .replaceAll('&#39;', "'")
                    .replaceAll('&apos;', "'")
                    .replaceAll('&amp;', '&');
                return JSON.parse(decoded);
            }
            catch (_b) {
                return;
            }
        }
    }
}
/**
 * Check whether a URL string uses a safe protocol for the given property.
 * Relative URLs, hash links, and protocol-relative URLs are always allowed.
 * @param value - The URL string to check.
 * @param allowedProtocols - The set of allowed protocols for this property.
 */
function isSafeUrl(value, allowedProtocols) {
    const trimmed = value.trim();
    const colonIndex = trimmed.indexOf(':');
    // No colon, or colon appears after ?, #, or / → relative URL, always safe
    if (colonIndex === -1 || /[?#/]/.test(trimmed.slice(0, colonIndex))) {
        return true;
    }
    const protocol = trimmed.slice(0, colonIndex).toLowerCase();
    return allowedProtocols.has(protocol);
}
/**
 * Recursively sanitize URL-bearing properties in a parsed JSON value.
 * Uses the same protocol allowlists as rehype-sanitize to block dangerous
 * schemes (e.g. `javascript:`, `data:`) while allowing safe ones.
 * Covers all URL properties that rehype-sanitize defines protocols for:
 * `href`, `src`, `cite`, and `longDesc`.
 * Unsafe URLs are removed to prevent script injection.
 * @param value
 */
function sanitizeUrls(value) {
    if (value === null || typeof value !== 'object') {
        return value;
    }
    if (Array.isArray(value)) {
        return value.map(sanitizeUrls);
    }
    const result = Object.assign({}, value);
    for (const key of Object.keys(result)) {
        const allowedProtocols = SAFE_PROTOCOLS_BY_PROPERTY.get(key);
        if (allowedProtocols &&
            typeof result[key] === 'string' &&
            !isSafeUrl(result[key], allowedProtocols)) {
            console.warn(`limel-markdown: Removed unsafe URL from "${key}" during sanitization.`);
            delete result[key];
        }
        else if (typeof result[key] === 'object' && result[key] !== null) {
            result[key] = sanitizeUrls(result[key]);
        }
    }
    return result;
}
/**
 * Convert a kebab-case attribute name to a camelCase property name.
 * e.g. "menu-items" → "menuItems"
 * @param attrName
 */
function attributeToPropName(attrName) {
    return attrName.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}

var DOCUMENT_FRAGMENT_NODE = 11;

function morphAttrs(fromNode, toNode) {
    var toNodeAttrs = toNode.attributes;
    var attr;
    var attrName;
    var attrNamespaceURI;
    var attrValue;
    var fromValue;

    // document-fragments dont have attributes so lets not do anything
    if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
      return;
    }

    // update attributes on original DOM element
    for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
        attr = toNodeAttrs[i];
        attrName = attr.name;
        attrNamespaceURI = attr.namespaceURI;
        attrValue = attr.value;

        if (attrNamespaceURI) {
            attrName = attr.localName || attrName;
            fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);

            if (fromValue !== attrValue) {
                if (attr.prefix === 'xmlns'){
                    attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
                }
                fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
            }
        } else {
            fromValue = fromNode.getAttribute(attrName);

            if (fromValue !== attrValue) {
                fromNode.setAttribute(attrName, attrValue);
            }
        }
    }

    // Remove any extra attributes found on the original DOM element that
    // weren't found on the target element.
    var fromNodeAttrs = fromNode.attributes;

    for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
        attr = fromNodeAttrs[d];
        attrName = attr.name;
        attrNamespaceURI = attr.namespaceURI;

        if (attrNamespaceURI) {
            attrName = attr.localName || attrName;

            if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
                fromNode.removeAttributeNS(attrNamespaceURI, attrName);
            }
        } else {
            if (!toNode.hasAttribute(attrName)) {
                fromNode.removeAttribute(attrName);
            }
        }
    }
}

var range; // Create a range object for efficently rendering strings to elements.
var NS_XHTML = 'http://www.w3.org/1999/xhtml';

var doc = typeof document === 'undefined' ? undefined : document;
var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();

function createFragmentFromTemplate(str) {
    var template = doc.createElement('template');
    template.innerHTML = str;
    return template.content.childNodes[0];
}

function createFragmentFromRange(str) {
    if (!range) {
        range = doc.createRange();
        range.selectNode(doc.body);
    }

    var fragment = range.createContextualFragment(str);
    return fragment.childNodes[0];
}

function createFragmentFromWrap(str) {
    var fragment = doc.createElement('body');
    fragment.innerHTML = str;
    return fragment.childNodes[0];
}

/**
 * This is about the same
 * var html = new DOMParser().parseFromString(str, 'text/html');
 * return html.body.firstChild;
 *
 * @method toElement
 * @param {String} str
 */
function toElement(str) {
    str = str.trim();
    if (HAS_TEMPLATE_SUPPORT) {
      // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
      // createContextualFragment doesn't support
      // <template> support not available in IE
      return createFragmentFromTemplate(str);
    } else if (HAS_RANGE_SUPPORT) {
      return createFragmentFromRange(str);
    }

    return createFragmentFromWrap(str);
}

/**
 * Returns true if two node's names are the same.
 *
 * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
 *       nodeName and different namespace URIs.
 *
 * @param {Element} a
 * @param {Element} b The target element
 * @return {boolean}
 */
function compareNodeNames(fromEl, toEl) {
    var fromNodeName = fromEl.nodeName;
    var toNodeName = toEl.nodeName;
    var fromCodeStart, toCodeStart;

    if (fromNodeName === toNodeName) {
        return true;
    }

    fromCodeStart = fromNodeName.charCodeAt(0);
    toCodeStart = toNodeName.charCodeAt(0);

    // If the target element is a virtual DOM node or SVG node then we may
    // need to normalize the tag name before comparing. Normal HTML elements that are
    // in the "http://www.w3.org/1999/xhtml"
    // are converted to upper case
    if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
        return fromNodeName === toNodeName.toUpperCase();
    } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
        return toNodeName === fromNodeName.toUpperCase();
    } else {
        return false;
    }
}

/**
 * Create an element, optionally with a known namespace URI.
 *
 * @param {string} name the element name, e.g. 'div' or 'svg'
 * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
 * its `xmlns` attribute or its inferred namespace.
 *
 * @return {Element}
 */
function createElementNS(name, namespaceURI) {
    return !namespaceURI || namespaceURI === NS_XHTML ?
        doc.createElement(name) :
        doc.createElementNS(namespaceURI, name);
}

/**
 * Copies the children of one DOM element to another DOM element
 */
function moveChildren(fromEl, toEl) {
    var curChild = fromEl.firstChild;
    while (curChild) {
        var nextChild = curChild.nextSibling;
        toEl.appendChild(curChild);
        curChild = nextChild;
    }
    return toEl;
}

function syncBooleanAttrProp(fromEl, toEl, name) {
    if (fromEl[name] !== toEl[name]) {
        fromEl[name] = toEl[name];
        if (fromEl[name]) {
            fromEl.setAttribute(name, '');
        } else {
            fromEl.removeAttribute(name);
        }
    }
}

var specialElHandlers = {
    OPTION: function(fromEl, toEl) {
        var parentNode = fromEl.parentNode;
        if (parentNode) {
            var parentName = parentNode.nodeName.toUpperCase();
            if (parentName === 'OPTGROUP') {
                parentNode = parentNode.parentNode;
                parentName = parentNode && parentNode.nodeName.toUpperCase();
            }
            if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
                if (fromEl.hasAttribute('selected') && !toEl.selected) {
                    // Workaround for MS Edge bug where the 'selected' attribute can only be
                    // removed if set to a non-empty value:
                    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
                    fromEl.setAttribute('selected', 'selected');
                    fromEl.removeAttribute('selected');
                }
                // We have to reset select element's selectedIndex to -1, otherwise setting
                // fromEl.selected using the syncBooleanAttrProp below has no effect.
                // The correct selectedIndex will be set in the SELECT special handler below.
                parentNode.selectedIndex = -1;
            }
        }
        syncBooleanAttrProp(fromEl, toEl, 'selected');
    },
    /**
     * The "value" attribute is special for the <input> element since it sets
     * the initial value. Changing the "value" attribute without changing the
     * "value" property will have no effect since it is only used to the set the
     * initial value.  Similar for the "checked" attribute, and "disabled".
     */
    INPUT: function(fromEl, toEl) {
        syncBooleanAttrProp(fromEl, toEl, 'checked');
        syncBooleanAttrProp(fromEl, toEl, 'disabled');

        if (fromEl.value !== toEl.value) {
            fromEl.value = toEl.value;
        }

        if (!toEl.hasAttribute('value')) {
            fromEl.removeAttribute('value');
        }
    },

    TEXTAREA: function(fromEl, toEl) {
        var newValue = toEl.value;
        if (fromEl.value !== newValue) {
            fromEl.value = newValue;
        }

        var firstChild = fromEl.firstChild;
        if (firstChild) {
            // Needed for IE. Apparently IE sets the placeholder as the
            // node value and vise versa. This ignores an empty update.
            var oldValue = firstChild.nodeValue;

            if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
                return;
            }

            firstChild.nodeValue = newValue;
        }
    },
    SELECT: function(fromEl, toEl) {
        if (!toEl.hasAttribute('multiple')) {
            var selectedIndex = -1;
            var i = 0;
            // We have to loop through children of fromEl, not toEl since nodes can be moved
            // from toEl to fromEl directly when morphing.
            // At the time this special handler is invoked, all children have already been morphed
            // and appended to / removed from fromEl, so using fromEl here is safe and correct.
            var curChild = fromEl.firstChild;
            var optgroup;
            var nodeName;
            while(curChild) {
                nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
                if (nodeName === 'OPTGROUP') {
                    optgroup = curChild;
                    curChild = optgroup.firstChild;
                    // handle empty optgroups
                    if (!curChild) {
                        curChild = optgroup.nextSibling;
                        optgroup = null;
                    }
                } else {
                    if (nodeName === 'OPTION') {
                        if (curChild.hasAttribute('selected')) {
                            selectedIndex = i;
                            break;
                        }
                        i++;
                    }
                    curChild = curChild.nextSibling;
                    if (!curChild && optgroup) {
                        curChild = optgroup.nextSibling;
                        optgroup = null;
                    }
                }
            }

            fromEl.selectedIndex = selectedIndex;
        }
    }
};

var ELEMENT_NODE = 1;
var DOCUMENT_FRAGMENT_NODE$1 = 11;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;

function noop() {}

function defaultGetNodeKey(node) {
  if (node) {
    return (node.getAttribute && node.getAttribute('id')) || node.id;
  }
}

function morphdomFactory(morphAttrs) {

  return function morphdom(fromNode, toNode, options) {
    if (!options) {
      options = {};
    }

    if (typeof toNode === 'string') {
      if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') {
        var toNodeHtml = toNode;
        toNode = doc.createElement('html');
        toNode.innerHTML = toNodeHtml;
      } else if (fromNode.nodeName === 'BODY') {
        var toNodeBody = toNode;
        toNode = doc.createElement('html');
        toNode.innerHTML = toNodeBody;
        // Extract the body element from the created HTML structure
        var bodyElement = toNode.querySelector('body');
        if (bodyElement) {
          toNode = bodyElement;
        }
      } else {
        toNode = toElement(toNode);
      }
    } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
      toNode = toNode.firstElementChild;
    }

    var getNodeKey = options.getNodeKey || defaultGetNodeKey;
    var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
    var onNodeAdded = options.onNodeAdded || noop;
    var onBeforeElUpdated = options.onBeforeElUpdated || noop;
    var onElUpdated = options.onElUpdated || noop;
    var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
    var onNodeDiscarded = options.onNodeDiscarded || noop;
    var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
    var skipFromChildren = options.skipFromChildren || noop;
    var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
    var childrenOnly = options.childrenOnly === true;

    // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
    var fromNodesLookup = Object.create(null);
    var keyedRemovalList = [];

    function addKeyedRemoval(key) {
      keyedRemovalList.push(key);
    }

    function walkDiscardedChildNodes(node, skipKeyedNodes) {
      if (node.nodeType === ELEMENT_NODE) {
        var curChild = node.firstChild;
        while (curChild) {

          var key = undefined;

          if (skipKeyedNodes && (key = getNodeKey(curChild))) {
            // If we are skipping keyed nodes then we add the key
            // to a list so that it can be handled at the very end.
            addKeyedRemoval(key);
          } else {
            // Only report the node as discarded if it is not keyed. We do this because
            // at the end we loop through all keyed elements that were unmatched
            // and then discard them in one final pass.
            onNodeDiscarded(curChild);
            if (curChild.firstChild) {
              walkDiscardedChildNodes(curChild, skipKeyedNodes);
            }
          }

          curChild = curChild.nextSibling;
        }
      }
    }

    /**
    * Removes a DOM node out of the original DOM
    *
    * @param  {Node} node The node to remove
    * @param  {Node} parentNode The nodes parent
    * @param  {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
    * @return {undefined}
    */
    function removeNode(node, parentNode, skipKeyedNodes) {
      if (onBeforeNodeDiscarded(node) === false) {
        return;
      }

      if (parentNode) {
        parentNode.removeChild(node);
      }

      onNodeDiscarded(node);
      walkDiscardedChildNodes(node, skipKeyedNodes);
    }

    // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
    // function indexTree(root) {
    //     var treeWalker = document.createTreeWalker(
    //         root,
    //         NodeFilter.SHOW_ELEMENT);
    //
    //     var el;
    //     while((el = treeWalker.nextNode())) {
    //         var key = getNodeKey(el);
    //         if (key) {
    //             fromNodesLookup[key] = el;
    //         }
    //     }
    // }

    // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
    //
    // function indexTree(node) {
    //     var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
    //     var el;
    //     while((el = nodeIterator.nextNode())) {
    //         var key = getNodeKey(el);
    //         if (key) {
    //             fromNodesLookup[key] = el;
    //         }
    //     }
    // }

    function indexTree(node) {
      if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
        var curChild = node.firstChild;
        while (curChild) {
          var key = getNodeKey(curChild);
          if (key) {
            fromNodesLookup[key] = curChild;
          }

          // Walk recursively
          indexTree(curChild);

          curChild = curChild.nextSibling;
        }
      }
    }

    indexTree(fromNode);

    function handleNodeAdded(el) {
      onNodeAdded(el);

      var curChild = el.firstChild;
      while (curChild) {
        var nextSibling = curChild.nextSibling;

        var key = getNodeKey(curChild);
        if (key) {
          var unmatchedFromEl = fromNodesLookup[key];
          // if we find a duplicate #id node in cache, replace `el` with cache value
          // and morph it to the child node.
          if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
            curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
            morphEl(unmatchedFromEl, curChild);
          } else {
            handleNodeAdded(curChild);
          }
        } else {
          // recursively call for curChild and it's children to see if we find something in
          // fromNodesLookup
          handleNodeAdded(curChild);
        }

        curChild = nextSibling;
      }
    }

    function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
      // We have processed all of the "to nodes". If curFromNodeChild is
      // non-null then we still have some from nodes left over that need
      // to be removed
      while (curFromNodeChild) {
        var fromNextSibling = curFromNodeChild.nextSibling;
        if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
          // Since the node is keyed it might be matched up later so we defer
          // the actual removal to later
          addKeyedRemoval(curFromNodeKey);
        } else {
          // NOTE: we skip nested keyed nodes from being removed since there is
          //       still a chance they will be matched up later
          removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
        }
        curFromNodeChild = fromNextSibling;
      }
    }

    function morphEl(fromEl, toEl, childrenOnly) {
      var toElKey = getNodeKey(toEl);

      if (toElKey) {
        // If an element with an ID is being morphed then it will be in the final
        // DOM so clear it out of the saved elements collection
        delete fromNodesLookup[toElKey];
      }

      if (!childrenOnly) {
        // optional
        var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
        if (beforeUpdateResult === false) {
          return;
        } else if (beforeUpdateResult instanceof HTMLElement) {
          fromEl = beforeUpdateResult;
          // reindex the new fromEl in case it's not in the same
          // tree as the original fromEl
          // (Phoenix LiveView sometimes returns a cloned tree,
          //  but keyed lookups would still point to the original tree)
          indexTree(fromEl);
        }

        // update attributes on original DOM element first
        morphAttrs(fromEl, toEl);
        // optional
        onElUpdated(fromEl);

        if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
          return;
        }
      }

      if (fromEl.nodeName !== 'TEXTAREA') {
        morphChildren(fromEl, toEl);
      } else {
        specialElHandlers.TEXTAREA(fromEl, toEl);
      }
    }

    function morphChildren(fromEl, toEl) {
      var skipFrom = skipFromChildren(fromEl, toEl);
      var curToNodeChild = toEl.firstChild;
      var curFromNodeChild = fromEl.firstChild;
      var curToNodeKey;
      var curFromNodeKey;

      var fromNextSibling;
      var toNextSibling;
      var matchingFromEl;

      // walk the children
      outer: while (curToNodeChild) {
        toNextSibling = curToNodeChild.nextSibling;
        curToNodeKey = getNodeKey(curToNodeChild);

        // walk the fromNode children all the way through
        while (!skipFrom && curFromNodeChild) {
          fromNextSibling = curFromNodeChild.nextSibling;

          if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
            curToNodeChild = toNextSibling;
            curFromNodeChild = fromNextSibling;
            continue outer;
          }

          curFromNodeKey = getNodeKey(curFromNodeChild);

          var curFromNodeType = curFromNodeChild.nodeType;

          // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
          var isCompatible = undefined;

          if (curFromNodeType === curToNodeChild.nodeType) {
            if (curFromNodeType === ELEMENT_NODE) {
              // Both nodes being compared are Element nodes

              if (curToNodeKey) {
                // The target node has a key so we want to match it up with the correct element
                // in the original DOM tree
                if (curToNodeKey !== curFromNodeKey) {
                  // The current element in the original DOM tree does not have a matching key so
                  // let's check our lookup to see if there is a matching element in the original
                  // DOM tree
                  if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
                    if (fromNextSibling === matchingFromEl) {
                      // Special case for single element removals. To avoid removing the original
                      // DOM node out of the tree (since that can break CSS transitions, etc.),
                      // we will instead discard the current node and wait until the next
                      // iteration to properly match up the keyed target element with its matching
                      // element in the original tree
                      isCompatible = false;
                    } else {
                      // We found a matching keyed element somewhere in the original DOM tree.
                      // Let's move the original DOM node into the current position and morph
                      // it.

                      // NOTE: We use insertBefore instead of replaceChild because we want to go through
                      // the `removeNode()` function for the node that is being discarded so that
                      // all lifecycle hooks are correctly invoked
                      fromEl.insertBefore(matchingFromEl, curFromNodeChild);

                      // fromNextSibling = curFromNodeChild.nextSibling;

                      if (curFromNodeKey) {
                        // Since the node is keyed it might be matched up later so we defer
                        // the actual removal to later
                        addKeyedRemoval(curFromNodeKey);
                      } else {
                        // NOTE: we skip nested keyed nodes from being removed since there is
                        //       still a chance they will be matched up later
                        removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
                      }

                      curFromNodeChild = matchingFromEl;
                      curFromNodeKey = getNodeKey(curFromNodeChild);
                    }
                  } else {
                    // The nodes are not compatible since the "to" node has a key and there
                    // is no matching keyed node in the source tree
                    isCompatible = false;
                  }
                }
              } else if (curFromNodeKey) {
                // The original has a key
                isCompatible = false;
              }

              isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
              if (isCompatible) {
                // We found compatible DOM elements so transform
                // the current "from" node to match the current
                // target DOM node.
                // MORPH
                morphEl(curFromNodeChild, curToNodeChild);
              }

            } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
              // Both nodes being compared are Text or Comment nodes
              isCompatible = true;
              // Simply update nodeValue on the original node to
              // change the text value
              if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
                curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
              }

            }
          }

          if (isCompatible) {
            // Advance both the "to" child and the "from" child since we found a match
            // Nothing else to do as we already recursively called morphChildren above
            curToNodeChild = toNextSibling;
            curFromNodeChild = fromNextSibling;
            continue outer;
          }

          // No compatible match so remove the old node from the DOM and continue trying to find a
          // match in the original DOM. However, we only do this if the from node is not keyed
          // since it is possible that a keyed node might match up with a node somewhere else in the
          // target tree and we don't want to discard it just yet since it still might find a
          // home in the final DOM tree. After everything is done we will remove any keyed nodes
          // that didn't find a home
          if (curFromNodeKey) {
            // Since the node is keyed it might be matched up later so we defer
            // the actual removal to later
            addKeyedRemoval(curFromNodeKey);
          } else {
            // NOTE: we skip nested keyed nodes from being removed since there is
            //       still a chance they will be matched up later
            removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
          }

          curFromNodeChild = fromNextSibling;
        } // END: while(curFromNodeChild) {}

        // If we got this far then we did not find a candidate match for
        // our "to node" and we exhausted all of the children "from"
        // nodes. Therefore, we will just append the current "to" node
        // to the end
        if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
          // MORPH
          if(!skipFrom){ addChild(fromEl, matchingFromEl); }
          morphEl(matchingFromEl, curToNodeChild);
        } else {
          var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
          if (onBeforeNodeAddedResult !== false) {
            if (onBeforeNodeAddedResult) {
              curToNodeChild = onBeforeNodeAddedResult;
            }

            if (curToNodeChild.actualize) {
              curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
            }
            addChild(fromEl, curToNodeChild);
            handleNodeAdded(curToNodeChild);
          }
        }

        curToNodeChild = toNextSibling;
        curFromNodeChild = fromNextSibling;
      }

      cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);

      var specialElHandler = specialElHandlers[fromEl.nodeName];
      if (specialElHandler) {
        specialElHandler(fromEl, toEl);
      }
    } // END: morphChildren(...)

    var morphedNode = fromNode;
    var morphedNodeType = morphedNode.nodeType;
    var toNodeType = toNode.nodeType;

    if (!childrenOnly) {
      // Handle the case where we are given two DOM nodes that are not
      // compatible (e.g. <div> --> <span> or <div> --> TEXT)
      if (morphedNodeType === ELEMENT_NODE) {
        if (toNodeType === ELEMENT_NODE) {
          if (!compareNodeNames(fromNode, toNode)) {
            onNodeDiscarded(fromNode);
            morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
          }
        } else {
          // Going from an element node to a text node
          morphedNode = toNode;
        }
      } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
        if (toNodeType === morphedNodeType) {
          if (morphedNode.nodeValue !== toNode.nodeValue) {
            morphedNode.nodeValue = toNode.nodeValue;
          }

          return morphedNode;
        } else {
          // Text node to something else
          morphedNode = toNode;
        }
      }
    }

    if (morphedNode === toNode) {
      // The "to node" was not compatible with the "from node" so we had to
      // toss out the "from node" and use the "to node"
      onNodeDiscarded(fromNode);
    } else {
      if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
        return;
      }

      morphEl(morphedNode, toNode, childrenOnly);

      // We now need to loop over any keyed nodes that might need to be
      // removed. We only do the removal if we know that the keyed node
      // never found a match. When a keyed node is matched up we remove
      // it out of fromNodesLookup and we use fromNodesLookup to determine
      // if a keyed node has been matched up or not
      if (keyedRemovalList) {
        for (var i=0, len=keyedRemovalList.length; i<len; i++) {
          var elToRemove = fromNodesLookup[keyedRemovalList[i]];
          if (elToRemove) {
            removeNode(elToRemove, elToRemove.parentNode, false);
          }
        }
      }
    }

    if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
      if (morphedNode.actualize) {
        morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
      }
      // If we had to swap out the from node with a new node because the old
      // node was not compatible with the target node then we need to
      // replace the old DOM node in the original DOM tree. This is only
      // possible if the original DOM node was part of a DOM tree which
      // we know is the case if it has a parent node.
      fromNode.parentNode.replaceChild(morphedNode, fromNode);
    }

    return morphedNode;
  };
}

var morphdom = morphdomFactory(morphAttrs);

/**
 * Morph the children of `container` to match the given HTML string.
 *
 * Uses morphdom to diff the existing DOM against the new HTML and apply
 * only the minimum changes. This preserves existing DOM nodes (including
 * custom elements with internal state) that haven't changed.
 *
 * @param container - The parent element whose children should be morphed.
 * @param html - The new HTML content for the container's children.
 */
function morphChildren(container, html = '') {
    // morphdom's second argument must be a single root element. We wrap
    // the new HTML in a <div> purely to satisfy that requirement — the
    // tag name doesn't matter because childrenOnly makes morphdom skip
    // the root and only diff the children. The container element itself
    // is never compared or replaced, so it can be any element type.
    try {
        morphdom(container, `<div>${html}</div>`, {
            childrenOnly: true,
        });
    }
    catch (error) {
        // Fall back to innerHTML so that content is at least visible,
        // even though custom elements will be destroyed and recreated.
        console.warn('morphdom failed, falling back to innerHTML:', error);
        container.innerHTML = html;
    }
}

/**
 * Default whitelist of lime-elements components that are safe to render
 * inside `limel-markdown`.
 *
 * These components are self-contained and require no complex setup.
 * URL-bearing properties (e.g. `link.href` on `limel-chip`) are
 * automatically sanitized during hydration to prevent injection attacks.
 *
 * Consumers can extend this list via the `whitelist` prop or
 * `limel-config` global config.
 *
 * @internal
 */
const DEFAULT_MARKDOWN_WHITELIST = [
    {
        tagName: 'limel-chip',
        attributes: [
            'text',
            'icon',
            'link',
            'badge',
            'disabled',
            'readonly',
            'selected',
            'type',
            'size',
        ],
    },
    {
        tagName: 'limel-icon',
        attributes: ['name', 'size', 'badge'],
    },
    {
        tagName: 'limel-badge',
        attributes: ['label'],
    },
    {
        tagName: 'limel-callout',
        attributes: ['heading', 'icon', 'type'],
    },
    {
        tagName: 'limel-linear-progress',
        attributes: ['value', 'indeterminate'],
    },
    {
        tagName: 'limel-circular-progress',
        attributes: [
            'value',
            'max-value',
            'prefix',
            'suffix',
            'size',
            'display-percentage-colors',
        ],
    },
    {
        tagName: 'limel-spinner',
        attributes: ['size'],
    },
    {
        tagName: 'limel-info-tile',
        attributes: ['value', 'icon', 'label', 'prefix', 'suffix', 'badge'],
    },
];

const markdownCss = () => `@charset "UTF-8";pre,pre p,code{font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-size:0.75rem}pre{max-width:100%;white-space:pre-wrap;word-wrap:break-word}code{letter-spacing:-0.0125rem;color:rgb(var(--contrast-1300));tab-size:4;hyphens:none;display:inline;border-radius:0.25rem;padding:0.03125rem 0.25rem;background-color:rgb(var(--contrast-600))}pre>code{display:block;margin:0.5rem 0;padding:0.5rem 0.75rem}h1{font-size:1.5rem}h2{font-size:1.25rem}h3{font-size:1.125rem}h4{font-size:1rem}h5{font-size:var(--limel-theme-default-font-size)}h6{font-size:0.75rem}h1,h2{margin-top:0.5rem;margin-bottom:0.5rem;letter-spacing:-0.03125rem;font-weight:500}h3,h4{margin-top:0.75rem;margin-bottom:0.25rem;font-weight:600}h5,h6{margin-top:0.5rem;margin-bottom:0.125rem;font-weight:600}h1,h2,h3,h4,h5,h6{word-break:break-word;hyphens:auto;-webkit-hyphens:auto}:not([contenteditable=true]) h1,:not([contenteditable=true]) h2,:not([contenteditable=true]) h3,:not([contenteditable=true]) h4,:not([contenteditable=true]) h5,:not([contenteditable=true]) h6{text-wrap:balance}[contenteditable=true] h1,[contenteditable=true] h2,[contenteditable=true] h3,[contenteditable=true] h4,[contenteditable=true] h5,[contenteditable=true] h6{text-wrap:initial}:host(limel-markdown.truncate-paragraphs) p{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}p,li{font-size:var(--limel-theme-default-font-size);word-break:break-word}a{word-break:break-all}p{margin-top:0;margin-bottom:0.5rem}p:only-child{margin-bottom:0}a{transition:color 0.2s ease;color:var(--markdown-hyperlink-color, rgb(var(--color-blue-dark)));text-decoration:none}a:hover{color:var(--markdown-hyperlink-color--hovered, rgb(var(--color-blue-default)))}hr{margin:1.75rem 0 2rem 0;border-width:0;border-top:1px solid rgb(var(--contrast-500))}ul{list-style:none}ul li{position:relative;margin-left:0.75rem}ul li:before{content:"";position:absolute;left:-0.5rem;top:0.5rem;width:0.25rem;height:0.25rem;border-radius:50%;background-color:rgb(var(--contrast-700));display:block}ol{margin-top:0.25rem;padding-left:1rem}ul{margin-top:0.25rem;padding-left:0}ul ul,ul ol,ol ol,ol ul{margin-left:0}li{margin-bottom:0.25rem}:host(limel-markdown:not(.no-table-styles)) table{table-layout:auto;min-width:100%;border-collapse:collapse;border-spacing:0;background:transparent;margin:0.75rem 0}:host(limel-markdown:not(.no-table-styles)) tbody{border:1px solid rgb(var(--contrast-400));border-radius:0.25rem}:host(limel-markdown:not(.no-table-styles)) th,:host(limel-markdown:not(.no-table-styles)) td{text-align:left;vertical-align:top;transition:background-color 0.2s ease;font-size:var(--limel-theme-default-font-size)}:host(limel-markdown:not(.no-table-styles)) td{padding:0.5rem 0.375rem 0.75rem 0.375rem}:host(limel-markdown:not(.no-table-styles)) tr th{background-color:rgb(var(--contrast-400));padding:0.25rem 0.375rem;font-weight:normal}:host(limel-markdown:not(.no-table-styles)) tr th:only-child{text-align:center}:host(limel-markdown:not(.no-table-styles)) tbody tr:nth-child(odd) td{background-color:rgb(var(--contrast-200))}:host(limel-markdown:not(.no-table-styles)) tbody tr:hover td{background-color:rgb(var(--contrast-300))}table{display:block;box-sizing:border-box;overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}blockquote{position:relative;max-width:100%;margin:0.75rem 0;padding:0.5rem;border-left:0.25rem solid rgb(var(--contrast-500));background-color:rgb(var(--contrast-200))}blockquote:before,blockquote:after{position:absolute;line-height:0;font-size:2rem;opacity:0.4}blockquote:before{content:"“";left:-0.5rem;top:0.5rem}blockquote:after{content:"”";right:-0.25rem;bottom:-0.25rem}blockquote blockquote{padding-top:0;padding-right:0;padding-bottom:0;padding-left:0.25rem;border-color:rgb(var(--contrast-700));border-left-width:1px}blockquote blockquote:before,blockquote blockquote:after{display:none}blockquote:has(>blockquote){padding-left:0.25rem;padding-bottom:0}dl{display:grid;grid-template-columns:1fr 2fr;grid-template-rows:1fr;margin-bottom:2rem;border:1px solid rgb(var(--contrast-400));border-radius:0.375rem;background-color:rgb(var(--contrast-200))}dl dt,dl dd{padding:0.375rem 0.5rem;font-size:var(--limel-theme-default-font-size);margin:0}dl dt:nth-of-type(even),dl dd:nth-of-type(even){background-color:rgb(var(--contrast-300))}dl dt:first-child{border-top-left-radius:0.375rem}dl dt:last-child{border-bottom-left-radius:0.375rem}dl dd:first-child{border-top-right-radius:0.375rem}dl dd:last-child{border-bottom-right-radius:0.375rem}img{max-width:100%;border-radius:0.25rem}kbd{display:inline-block;font-family:ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;font-weight:600;color:rgb(var(--contrast-1100));white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:normal;border-radius:0.125rem;border-style:solid;border-color:rgb(var(--contrast-500));border-width:0 1px 0.1875rem 1px;padding:0.0625rem 0.375rem;margin:0 0.25rem;background-color:rgb(var(--contrast-200));box-shadow:var(--button-shadow-normal), 0 0.625rem 0.375rem -0.5rem rgb(var(--color-black), 0.02), 0 0.025rem 0.5rem 0 rgb(var(--contrast-100)) inset}:host(limel-markdown.adjust-for-table-cell) img{max-height:1.25rem;vertical-align:middle}:host(limel-markdown.adjust-for-table-cell) p{display:inline}:host(limel-markdown.adjust-for-table-cell) h1,:host(limel-markdown.adjust-for-table-cell) h2,:host(limel-markdown.adjust-for-table-cell) h3,:host(limel-markdown.adjust-for-table-cell) h4,:host(limel-markdown.adjust-for-table-cell) h5,:host(limel-markdown.adjust-for-table-cell) h6{display:inline-block;vertical-align:bottom;font-size:var(--limel-theme-default-font-size);margin:0 0.25rem 0 0;letter-spacing:normal;font-weight:500}:host(limel-markdown.adjust-for-table-cell) h1:before,:host(limel-markdown.adjust-for-table-cell) h2:before,:host(limel-markdown.adjust-for-table-cell) h3:before,:host(limel-markdown.adjust-for-table-cell) h4:before,:host(limel-markdown.adjust-for-table-cell) h5:before,:host(limel-markdown.adjust-for-table-cell) h6:before{opacity:0.6;vertical-align:middle;font-size:0.5rem;border-radius:0.25rem 0 0 0.25rem;padding:0.25rem;padding-right:2rem;margin-right:-1.75rem;background:linear-gradient(to right, rgb(var(--contrast-800), 0.6), rgb(var(--contrast-800), 0))}:host(limel-markdown.adjust-for-table-cell) h1:before{content:"H1"}:host(limel-markdown.adjust-for-table-cell) h2:before{content:"H2"}:host(limel-markdown.adjust-for-table-cell) h3:before{content:"H3"}:host(limel-markdown.adjust-for-table-cell) h4:before{content:"H4"}:host(limel-markdown.adjust-for-table-cell) h5:before{content:"H5"}:host(limel-markdown.adjust-for-table-cell) h6:before{content:"H6"}:host(limel-markdown.adjust-for-table-cell) pre{margin:0}:host(limel-markdown.adjust-for-table-cell) pre>code{padding:0.125rem;margin:0}:host(limel-markdown.adjust-for-table-cell) dl{margin:0}:host(limel-markdown.adjust-for-table-cell) dl dt,:host(limel-markdown.adjust-for-table-cell) dl dd{padding:0.00625rem 0.125rem}*,*::before,*::after{box-sizing:border-box}* :where(:not(img,video,svg,canvas,iframe)),*::before :where(:not(img,video,svg,canvas,iframe)),*::after :where(:not(img,video,svg,canvas,iframe)){min-width:0;min-height:0}hr{border-top:1px solid rgb(var(--contrast-700))}.MsoNormal{margin:0}:host(limel-markdown.reset-img-height) #markdown img{height:auto}`;

const Markdown = class {
    constructor(hostRef) {
        index$1.registerInstance(this, hostRef);
        /**
         * The input text. Treated as GitHub Flavored Markdown, with the addition
         * that any included HTML will be parsed and rendered as HTML, rather than
         * as text.
         */
        this.value = '';
        /**
         * Additional whitelisted custom elements to render inside markdown.
         *
         * A built-in set of lime-elements components (such as `limel-chip`,
         * `limel-icon`, `limel-badge`, `limel-callout`, etc.) is always
         * allowed by default. Any entries provided here are **merged** with
         * those defaults — if both define the same `tagName`, their
         * attributes are combined.
         *
         * Can also be set via `limel-config`. Setting this property will
         * override the global config.
         *
         * JSON attribute values that contain URL-bearing properties
         * (`href`, `src`, `cite`, `longDesc`) are automatically sanitized
         * using the same protocol allowlists as rehype-sanitize. URLs with
         * dangerous schemes (e.g. `javascript:`, `data:`) are removed
         * (with a console warning).
         *
         * @alpha
         */
        this.whitelist = config.globalConfig.markdownWhitelist;
        /**
         * Enable lazy loading for images
         */
        this.lazyLoadImages = false;
        /**
         * Set to `false` to preserve empty paragraphs before rendering.
         * Empty paragraphs are paragraphs that do not contain
         * any meaningful content (text, images, etc.), or only contain
         * whitespace (`<br />` or `&nbsp;`).
         */
        this.removeEmptyParagraphs = true;
        this.imageIntersectionObserver = null;
    }
    async textChanged() {
        try {
            this.cleanupImageIntersectionObserver();
            // The whitelist merge and default import live here (not in
            // markdown-parser.ts) because this component orchestrates both
            // the parser and hydration, which both need the combined list.
            if (!this.cachedCombinedWhitelist ||
                this.whitelist !== this.cachedConsumerWhitelist) {
                this.cachedConsumerWhitelist = this.whitelist;
                this.cachedCombinedWhitelist = mergeWhitelists(DEFAULT_MARKDOWN_WHITELIST, this.whitelist);
            }
            const combinedWhitelist = this.cachedCombinedWhitelist;
            const html = await markdownParser.markdownToHTML(this.value, {
                forceHardLineBreaks: true,
                whitelist: combinedWhitelist,
                lazyLoadImages: this.lazyLoadImages,
                removeEmptyParagraphs: this.removeEmptyParagraphs,
            });
            morphChildren(this.rootElement, html);
            // Hydration parses JSON attribute values (e.g. link='{"href":"..."}')
            // into JS properties. URL sanitization happens here because
            // rehype-sanitize can't inspect values inside JSON strings.
            hydrateCustomElements(this.rootElement, combinedWhitelist);
            this.setupImageIntersectionObserver();
        }
        catch (error) {
            console.error(error);
        }
    }
    handleWhitelistChange() {
        return this.textChanged();
    }
    handleRemoveEmptyParagraphsChange() {
        return this.textChanged();
    }
    async componentDidLoad() {
        this.textChanged();
    }
    disconnectedCallback() {
        this.cleanupImageIntersectionObserver();
    }
    render() {
        return (index$1.h(index$1.Host, { key: '9d88a42e047b6701215699ab5459af3275e51693' }, index$1.h("div", { key: '83a5801839318bf47eeacbf53e320e4628559bfa', id: "markdown", ref: (el) => (this.rootElement = el) })));
    }
    setupImageIntersectionObserver() {
        if (this.lazyLoadImages) {
            this.imageIntersectionObserver = new ImageIntersectionObserver(this.rootElement);
        }
    }
    cleanupImageIntersectionObserver() {
        if (this.imageIntersectionObserver) {
            this.imageIntersectionObserver.disconnect();
            this.imageIntersectionObserver = null;
        }
    }
    static get watchers() { return {
        "value": [{
                "textChanged": 0
            }],
        "whitelist": [{
                "handleWhitelistChange": 0
            }],
        "removeEmptyParagraphs": [{
                "handleRemoveEmptyParagraphsChange": 0
            }]
    }; }
};
/**
 * Merge the default whitelist with a consumer-provided one.
 * If both define the same tagName, their attributes are combined.
 * @param defaults
 * @param consumer
 */
function mergeWhitelists(defaults, consumer) {
    if (!(consumer === null || consumer === void 0 ? void 0 : consumer.length)) {
        return defaults.map((def) => (Object.assign(Object.assign({}, def), { attributes: [...def.attributes] })));
    }
    const merged = new Map();
    for (const def of [...defaults, ...consumer]) {
        const existing = merged.get(def.tagName);
        if (existing) {
            for (const attr of def.attributes) {
                existing.add(attr);
            }
        }
        else {
            merged.set(def.tagName, new Set(def.attributes));
        }
    }
    return [...merged.entries()].map(([tagName, attrs]) => ({
        tagName,
        attributes: [...attrs],
    }));
}
Markdown.style = markdownCss();

exports.limel_markdown = Markdown;
