import { reset, addDisposableListener } from "./dom.mjs";
import { addHook, sanitize, removeHook } from "./dompurify/dompurify.mjs";
import { DomEmitter } from "./event.mjs";
import { createElement } from "./formattedTextRenderer.mjs";
import { StandardMouseEvent } from "./mouseEvent.mjs";
import { renderLabelWithIcons } from "./ui/iconLabel/iconLabels.mjs";
import { raceCancellation } from "../common/async.mjs";
import { CancellationTokenSource } from "../common/cancellation.mjs";
import { onUnexpectedError } from "../common/errors.mjs";
import { Event } from "../common/event.mjs";
import { parseHrefAndDimensions, removeMarkdownEscapes } from "../common/htmlContent.mjs";
import { markdownEscapeEscapedIcons } from "../common/iconLabels.mjs";
import { defaultGenerator } from "../common/idGenerator.mjs";
import { DisposableStore } from "../common/lifecycle.mjs";
import { marked } from "../common/marked/marked.mjs";
import { parse } from "../common/marshalling.mjs";
import { Schemas, FileAccess } from "../common/network.mjs";
import { cloneAndChange } from "../common/objects.mjs";
import { resolvePath, dirname } from "../common/resources.mjs";
import { escape } from "../common/strings.mjs";
import { URI } from "../common/uri.mjs";
function renderMarkdown(markdown, options = {}, markedOptions = {}) {
  var _a;
  const disposables = new DisposableStore();
  let isDisposed = false;
  const cts = disposables.add(new CancellationTokenSource());
  const element = createElement(options);
  const _uriMassage = function(part) {
    let data;
    try {
      data = parse(decodeURIComponent(part));
    } catch (e) {
    }
    if (!data) {
      return part;
    }
    data = cloneAndChange(data, (value2) => {
      if (markdown.uris && markdown.uris[value2]) {
        return URI.revive(markdown.uris[value2]);
      } else {
        return void 0;
      }
    });
    return encodeURIComponent(JSON.stringify(data));
  };
  const _href = function(href, isDomUri) {
    const data = markdown.uris && markdown.uris[href];
    let uri = URI.revive(data);
    if (isDomUri) {
      if (href.startsWith(Schemas.data + ":")) {
        return href;
      }
      if (!uri) {
        uri = URI.parse(href);
      }
      return FileAccess.asBrowserUri(uri).toString(true);
    }
    if (!uri) {
      return href;
    }
    if (URI.parse(href).toString() === uri.toString()) {
      return href;
    }
    if (uri.query) {
      uri = uri.with({ query: _uriMassage(uri.query) });
    }
    return uri.toString();
  };
  let signalInnerHTML;
  const withInnerHTML = new Promise((c) => signalInnerHTML = c);
  const renderer = new marked.Renderer();
  renderer.image = (href, title, text) => {
    let dimensions = [];
    let attributes = [];
    if (href) {
      ({ href, dimensions } = parseHrefAndDimensions(href));
      attributes.push(`src="${href}"`);
    }
    if (text) {
      attributes.push(`alt="${text}"`);
    }
    if (title) {
      attributes.push(`title="${title}"`);
    }
    if (dimensions.length) {
      attributes = attributes.concat(dimensions);
    }
    return "<img " + attributes.join(" ") + ">";
  };
  renderer.link = (href, title, text) => {
    if (typeof href !== "string") {
      return "";
    }
    if (href === text) {
      text = removeMarkdownEscapes(text);
    }
    href = _href(href, false);
    if (markdown.baseUri) {
      href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
    }
    title = typeof title === "string" ? removeMarkdownEscapes(title) : "";
    href = removeMarkdownEscapes(href);
    if (!href || /^data:|javascript:/i.test(href) || /^command:/i.test(href) && !markdown.isTrusted || /^command:(\/\/\/)?_workbench\.downloadResource/i.test(href)) {
      return text;
    } else {
      href = href.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
      return `<a data-href="${href}" title="${title || href}">${text}</a>`;
    }
  };
  renderer.paragraph = (text) => {
    return `<p>${text}</p>`;
  };
  if (options.codeBlockRenderer) {
    renderer.code = (code, lang) => {
      const value2 = options.codeBlockRenderer(lang !== null && lang !== void 0 ? lang : "", code);
      const id = defaultGenerator.nextId();
      raceCancellation(Promise.all([value2, withInnerHTML]), cts.token).then((values) => {
        var _a2;
        if (!isDisposed && values) {
          const span = element.querySelector(`div[data-code="${id}"]`);
          if (span) {
            reset(span, values[0]);
          }
          (_a2 = options.asyncRenderCallback) === null || _a2 === void 0 ? void 0 : _a2.call(options);
        }
      }).catch(() => {
      });
      return `<div class="code" data-code="${id}">${escape(code)}</div>`;
    };
  }
  if (options.actionHandler) {
    const onClick = options.actionHandler.disposables.add(new DomEmitter(element, "click"));
    const onAuxClick = options.actionHandler.disposables.add(new DomEmitter(element, "auxclick"));
    options.actionHandler.disposables.add(Event.any(onClick.event, onAuxClick.event)((e) => {
      const mouseEvent = new StandardMouseEvent(e);
      if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
        return;
      }
      let target = mouseEvent.target;
      if (target.tagName !== "A") {
        target = target.parentElement;
        if (!target || target.tagName !== "A") {
          return;
        }
      }
      try {
        let href = target.dataset["href"];
        if (href) {
          if (markdown.baseUri) {
            href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
          }
          options.actionHandler.callback(href, mouseEvent);
        }
      } catch (err) {
        onUnexpectedError(err);
      } finally {
        mouseEvent.preventDefault();
      }
    }));
  }
  if (!markdown.supportHtml) {
    markedOptions.sanitizer = (html) => {
      const match = markdown.isTrusted ? html.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : void 0;
      return match ? html : "";
    };
    markedOptions.sanitize = true;
    markedOptions.silent = true;
  }
  markedOptions.renderer = renderer;
  let value = (_a = markdown.value) !== null && _a !== void 0 ? _a : "";
  if (value.length > 1e5) {
    value = `${value.substr(0, 1e5)}…`;
  }
  if (markdown.supportThemeIcons) {
    value = markdownEscapeEscapedIcons(value);
  }
  let renderedMarkdown = marked.parse(value, markedOptions);
  if (markdown.supportThemeIcons) {
    const elements = renderLabelWithIcons(renderedMarkdown);
    renderedMarkdown = elements.map((e) => typeof e === "string" ? e : e.outerHTML).join("");
  }
  const htmlParser = new DOMParser();
  const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown(markdown, renderedMarkdown), "text/html");
  markdownHtmlDoc.body.querySelectorAll("img").forEach((img) => {
    const src = img.getAttribute("src");
    if (src) {
      let href = src;
      try {
        if (markdown.baseUri) {
          href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
        }
      } catch (err) {
      }
      img.src = _href(href, true);
    }
  });
  element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML);
  signalInnerHTML();
  if (options.asyncRenderCallback) {
    for (const img of element.getElementsByTagName("img")) {
      const listener = disposables.add(addDisposableListener(img, "load", () => {
        listener.dispose();
        options.asyncRenderCallback();
      }));
    }
  }
  return {
    element,
    dispose: () => {
      isDisposed = true;
      cts.cancel();
      disposables.dispose();
    }
  };
}
function resolveWithBaseUri(baseUri, href) {
  const hasScheme = /^\w[\w\d+.-]*:/.test(href);
  if (hasScheme) {
    return href;
  }
  if (baseUri.path.endsWith("/")) {
    return resolvePath(baseUri, href).toString();
  } else {
    return resolvePath(dirname(baseUri), href).toString();
  }
}
function sanitizeRenderedMarkdown(options, renderedMarkdown) {
  const { config, allowedSchemes } = getSanitizerOptions(options);
  addHook("uponSanitizeAttribute", (element, e) => {
    if (e.attrName === "style" || e.attrName === "class") {
      if (element.tagName === "SPAN") {
        if (e.attrName === "style") {
          e.keepAttr = /^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/.test(e.attrValue);
          return;
        } else if (e.attrName === "class") {
          e.keepAttr = /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(e.attrValue);
          return;
        }
      }
      e.keepAttr = false;
      return;
    }
  });
  const anchor = document.createElement("a");
  addHook("afterSanitizeAttributes", (node) => {
    for (const attr of ["href", "src"]) {
      if (node.hasAttribute(attr)) {
        anchor.href = node.getAttribute(attr);
        if (!allowedSchemes.includes(anchor.protocol.replace(/:$/, ""))) {
          node.removeAttribute(attr);
        }
      }
    }
  });
  try {
    return sanitize(renderedMarkdown, Object.assign(Object.assign({}, config), { RETURN_TRUSTED_TYPE: true }));
  } finally {
    removeHook("uponSanitizeAttribute");
    removeHook("afterSanitizeAttributes");
  }
}
function getSanitizerOptions(options) {
  const allowedSchemes = [
    Schemas.http,
    Schemas.https,
    Schemas.mailto,
    Schemas.data,
    Schemas.file,
    Schemas.vscodeFileResource,
    Schemas.vscodeRemote,
    Schemas.vscodeRemoteResource
  ];
  if (options.isTrusted) {
    allowedSchemes.push(Schemas.command);
  }
  return {
    config: {
      // allowedTags should included everything that markdown renders to.
      // Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure.
      // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
      // HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
      ALLOWED_TAGS: ["ul", "li", "p", "b", "i", "code", "blockquote", "ol", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "em", "pre", "table", "thead", "tbody", "tr", "th", "td", "div", "del", "a", "strong", "br", "img", "span"],
      ALLOWED_ATTR: ["href", "data-href", "target", "title", "src", "alt", "class", "style", "data-code", "width", "height", "align"],
      ALLOW_UNKNOWN_PROTOCOLS: true
    },
    allowedSchemes
  };
}
export {
  renderMarkdown
};
