Bad ChatGPT’s markdown in copy-paste: WORKING SOLUTION

ChatGPT UI update on October 8th appears to have broken rich-text copying to markdown, without preserving bold, italic, and bold-italic formatting, and making links to external websites replaced by links to google favicons.
The resulting markdown is as bad as it can be.

After several communications with the support and no resolution provided, I wrote the JS that you can insert into the browser console (press F12, and in the opened panel enter this JS code) that will return the ability to copy classic markdown to the clipboard. It adds a “Copy normal Markdown” button to all individual ChatGPT responses, and the resulting markdown preserves headings, bold and italic text, links, lists, tables, and code blocks.

Enjoy the script and support Scientific-Tools{dot}Org via Scientific-Tools{dot}Org{slash}support-us if you find this script useful.


/**
 * ChatGPT Classic Markdown Extractor
 *
 * @name        ChatGPT Classic Markdown Extractor
 * @description Adds a "Copy normal Markdown" button to ChatGPT responses.
 * @author      Scientific-Tools.Org
 * @version     1.0.0
 * @created     2026-10-12
 * @license     MIT
 *
 * Converts rendered ChatGPT responses into standard Markdown (thus avoiding bad markdown recently introduced by the OpenAI UI update) and copies the result directly to the clipboard.
 * This JavaScript tool that adds a “Copy normal Markdown” button to all individual ChatGPT responses. It converts rendered answers into standard Markdown, preserving headings, bold and italic text, links, lists, tables, and code blocks. Clicking the button copies Markdown to the clipboard.
 * Note: This experimental script may not fully support interactive elements or Mermaid diagrams and can interfere with ChatGPT's interface.
 * If you need to remove the existing buttons and observer
 * window.__normalMarkdown.stop();
 * or 
 * window.__normalMarkdown?.stop?.()
 */
 

(() => {
  "use strict";

  // Stop an earlier installation.
  window.__normalMarkdown?.stop?.();

  const ROOT_SELECTOR = '[class*="DilResponseRoot-"]';
  const BUTTON_CLASS = "normal-markdown-copy-button";

  let observer;
  let scanTimer;
  const buttons = new Set();

  // ---------- Helpers ----------

  const getText = el => el.textContent || "";

  const escapeMD = text =>
    text.replace(/\\/g, "\\\\")
        .replace(/([*_`[\]])/g, "\\$1");

  const isFavicon = el => {
    const src = el.getAttribute("src") || "";
    return /favicon|google\.com\/s2\/favicons/i.test(src);
  };

  const ignored = el =>
    el.matches(
      "script,style,noscript,button,svg," +
      "input,textarea,select,[aria-hidden='true']"
    ) ||
    el.closest("." + BUTTON_CLASS);

  const weight = value => {
    if (value === "bold") return 700;
    if (value === "normal") return 400;
    return parseInt(value, 10) || 400;
  };

  function wrap(text, marker) {
    if (!text.trim()) return text;

    const before = text.match(/^\s*/)[0];
    const after = text.match(/\s*$/)[0];
    const middle = text.slice(
      before.length,
      text.length - after.length
    );

    return before + marker + middle + marker + after;
  }

  // ---------- Inline formatting ----------

  function inline(node, parent = null) {
    if (node.nodeType === Node.TEXT_NODE) {
      return escapeMD(
        node.nodeValue.replace(/\s+/g, " ")
      );
    }

    if (node.nodeType !== Node.ELEMENT_NODE) return "";

    const el = node;
    const tag = el.tagName.toLowerCase();

    if (ignored(el)) return "";

    if (tag === "br") return "\n";

    if (tag === "img") {
      if (isFavicon(el)) return "";

      const src = el.getAttribute("src") || "";
      const alt = el.getAttribute("alt") || "";

      return src && alt
        ? `![${escapeMD(alt)}](${src})`
        : "";
    }

    if (tag === "code" && !el.closest("pre")) {
      const text = getText(el);
      const runs = [...text.matchAll(/`+/g)]
        .map(m => m[0].length);

      const fence = "`".repeat(
        Math.max(1, ...runs.map(n => n + 1))
      );

      return fence + text + fence;
    }

    let result = [...el.childNodes]
      .map(child => inline(child, el))
      .join("");

    if (tag === "a") {
      const href = el.getAttribute("href");

      if (!href) return result;

      let url;
      try {
        url = new URL(href, location.href);
      } catch {
        return result;
      }

      if (!["http:", "https:", "mailto:"].includes(
        url.protocol
      )) return result;

      if (/google\.com\/s2\/favicons/.test(url.href)) {
        return "";
      }

      return `[${result.trim() || url.href}](${
        url.href.replace(/\)/g, "%29")
      })`;
    }

    if (!result.trim()) return result;

    if (tag === "strong" || tag === "b") {
      return wrap(result, "**");
    }

    if (tag === "em" || tag === "i") {
      return wrap(result, "*");
    }

    if (["del", "s", "strike"].includes(tag)) {
      return wrap(result, "~~");
    }

    // Recover CSS-based bold and italic.
    if (parent && ![
      "p", "li", "a", "pre", "code",
      "h1", "h2", "h3", "h4", "h5", "h6"
    ].includes(tag)) {
      const cs = getComputedStyle(el);
      const ps = getComputedStyle(parent);

      const bold =
        weight(cs.fontWeight) >= 600 &&
        weight(ps.fontWeight) < 600;

      const italic =
        cs.fontStyle === "italic" &&
        ps.fontStyle !== "italic";

      if (bold && italic) return wrap(result, "***");
      if (bold) return wrap(result, "**");
      if (italic) return wrap(result, "*");
    }

    return result;
  }

  function inlineChildren(el) {
    return [...el.childNodes]
      .map(child => inline(child, el))
      .join("")
      .trim();
  }

  // ---------- Markdown blocks ----------

  function convert(root) {
    const output = [];

    const add = text => {
      if (text?.trim()) output.push(text.trim());
    };

    function convertList(el, depth = 0) {
      const ordered = el.tagName === "OL";
      let number = Number(el.getAttribute("start") || 1);

      return [...el.children]
        .filter(li => li.tagName === "LI")
        .map(li => {
          const nested = [...li.children]
            .filter(c => /^(UL|OL)$/.test(c.tagName));

          const text = [...li.childNodes]
            .filter(n =>
              n.nodeType !== Node.ELEMENT_NODE ||
              !/^(UL|OL)$/.test(n.tagName)
            )
            .map(n => inline(n, li))
            .join("")
            .trim();

          const checkbox = li.querySelector(
            'input[type="checkbox"],[role="checkbox"]'
          );

          let prefix;

          if (checkbox) {
            const checked =
              checkbox.checked ||
              checkbox.getAttribute("aria-checked") === "true";

            prefix = `- [${checked ? "x" : " "}] `;
          } else {
            prefix = ordered ? `${number++}. ` : "- ";
          }

          const line =
            "  ".repeat(depth) +
            prefix +
            text;

          const children = nested.map(n =>
            convertList(n, depth + 1)
          ).join("\n");

          return line + (children ? "\n" + children : "");
        })
        .join("\n");
    }

    function convertTable(el) {
      const rows = [...el.querySelectorAll("tr")]
        .map(tr =>
          [...tr.children]
            .filter(c => /^(TD|TH)$/.test(c.tagName))
            .map(c =>
              inlineChildren(c)
                .replace(/\|/g, "\\|")
                .replace(/\n/g, " ")
            )
        ).filter(r => r.length);

      if (!rows.length) return "";

      const cols = Math.max(...rows.map(r => r.length));

      const row = r =>
        "| " +
        Array.from(
          { length: cols },
          (_, i) => r[i] || ""
        ).join(" | ") +
        " |";

      return [
        row(rows[0]),
        row(Array(cols).fill("---")),
        ...rows.slice(1).map(row)
      ].join("\n");
    }

    function visit(el) {
      if (ignored(el)) return;

      const tag = el.tagName.toLowerCase();

		// Avoid exporting rendered Mermaid SVG and styling.
		if (
		  el.matches(
			'.mermaid, [class*="mermaid"], ' +
			'[id^="chatgpt-mermaid-"]'
		  )
		) {
		  const source = el.getAttribute("data-mermaid-source");

		  if (source) {
			add("```mermaid\n" + source + "\n```");
		  } else {
			add("<!-- Mermaid diagram: source unavailable -->");
		  }

		  return;
		}


      if (tag === "pre") {
        const code = el.querySelector("code") || el;
        const source = getText(code).replace(/\n$/, "");

        const classes =
          String(code.className || "") + " " +
          String(el.className || "");

        const lang =
          classes.match(/(?:language-|lang-)([\w+#-]+)/)?.[1]
          || "";

        const runs = [...source.matchAll(/`+/g)]
          .map(m => m[0].length);

        const fence = "`".repeat(
          Math.max(3, ...runs.map(n => n + 1))
        );

        add(`${fence}${lang}\n${source}\n${fence}`);
        return;
      }

      if (/^h[1-6]$/.test(tag)) {
        add("#".repeat(Number(tag[1])) +
            " " + inlineChildren(el));
        return;
      }

      if (tag === "ul" || tag === "ol") {
        add(convertList(el));
        return;
      }

      if (tag === "table") {
        add(convertTable(el));
        return;
      }

		if (tag === "blockquote") {
		  const container = document.createElement("div");

		  for (const child of el.childNodes) {
			container.appendChild(child.cloneNode(true));
		  }

		  const text = convert(container);

		  add(
			text.split("\n")
			  .map(line => line ? "> " + line : ">")
			  .join("\n")
		  );

		  return;
		}
		
      if (tag === "hr") {
        add("---");
        return;
      }

      if (tag === "p") {
        add(inlineChildren(el));
        return;
      }

      if (tag === "img") {
        add(inline(el, el.parentElement));
        return;
      }

      const blockTags =
        /^(P|DIV|SECTION|ARTICLE|H[1-6]|UL|OL|PRE|TABLE|BLOCKQUOTE|HR)$/;

      const hasBlocks = [...el.children]
        .some(c => blockTags.test(c.tagName));

      if (hasBlocks) {
        for (const child of el.childNodes) {
          if (child.nodeType === Node.ELEMENT_NODE) {
            visit(child);
          } else if (
            child.nodeType === Node.TEXT_NODE &&
            child.textContent.trim()
          ) {
            add(escapeMD(child.textContent.trim()));
          }
        }
      } else {
        add(inlineChildren(el));
      }
    }

    // Process root itself if appropriate.
    if (/^(P|PRE|UL|OL|TABLE|BLOCKQUOTE|H[1-6])$/
      .test(root.tagName)) {
      visit(root);
    } else {
      [...root.children].forEach(visit);
    }

    return output.join("\n\n")
      .replace(/\n{3,}/g, "\n\n")
      .trim();
  }

  // ---------- Popup ----------

  function openPopup(markdown) {
    const popup = window.open(
      "",
      "classic_markdown_popup",
      "width=1000,height=780,resizable=yes,scrollbars=yes"
    );

    if (!popup) {
      alert("Allow popups for chatgpt.com.");
      return;
    }

    const d = popup.document;
    d.title = "ChatGPT - Classic Markdown";
    d.body.replaceChildren();

    d.body.style.cssText =
      "margin:0;padding:20px;background:#f6f6f6;" +
      "font-family:system-ui;color:#222";

    const heading = d.createElement("h2");
    heading.textContent = "Classic Markdown";

    const info = d.createElement("p");
    info.textContent =
      "Markdown reconstructed from one ChatGPT answer.";

    const area = d.createElement("textarea");
    area.value = markdown;
    area.spellcheck = false;

	area.wrap = "soft";

	area.style.cssText =
	  "box-sizing:border-box;width:100%;height:72vh;" +
	  "padding:15px;font:13px/1.5 monospace;" +
	  "white-space:pre-wrap;" +
	  "overflow-wrap:anywhere;" +
	  "overflow-x:hidden;";

    const copy = d.createElement("button");
    copy.textContent = "Copy Markdown";

    copy.style.cssText =
      "padding:12px 20px;margin:12px 0;cursor:pointer";

    const status = d.createElement("span");
    status.style.marginLeft = "12px";

    copy.onclick = async () => {
      try {
        await popup.navigator.clipboard.writeText(area.value);
        status.textContent = "Copied!";
      } catch {
        area.focus();
        area.select();

        const ok = d.execCommand("copy");

        status.textContent = ok
          ? "Copied!"
          : "Press Ctrl+C to copy";
      }
    };

    d.body.append(
      heading,
      info,
      copy,
      status,
      area
    );

    popup.focus();
  }

  // ---------- Button injection ----------

  function inject(root) {
    if (root.dataset.normalMarkdownInstalled) return;

    const button = document.createElement("button");
    button.className = BUTTON_CLASS;
    button.type = "button";
    button.textContent = "Copy normal Markdown";
	button.title =
	  "ChatGPT Classic Markdown Extractor v1.0.0\n" +
	  "Created by Scientific-Tools.Org";
  
    button.style.cssText = [
      "display:inline-block",
      "margin:10px 0",
      "padding:8px 14px",
      "border:1px solid #999",
      "border-radius:8px",
      "background:#e9e9e9",
      "color:#111",
      "font:13px system-ui",
      "cursor:pointer",
      "position:relative",
      "z-index:10"
    ].join(";");

/*
// Version for popup window with normal markdown code in it
    button.onclick = event => {
      event.preventDefault();
      event.stopPropagation();

      const markdown = convert(root);

      if (!markdown) {
        alert("No Markdown content extracted.");
        return;
      }

      openPopup(markdown);
    };
*/

// Version that just copies normal markdown code without a popup window
button.onclick = async event => {
  event.preventDefault();
  event.stopPropagation();

  try {
    const markdown = convert(root);

    if (!markdown) {
      alert("No Markdown content extracted.");
      return;
    }

    await navigator.clipboard.writeText(markdown);

    // Brief visual confirmation
    const originalText = button.textContent;
    button.textContent = "✓ Markdown copied!";

    setTimeout(() => {
      if (button.isConnected) {
        button.textContent = originalText;
      }
    }, 1500);

  } catch (error) {
    console.error("Markdown copy failed:", error);
    alert("Could not copy Markdown: " + error.message);
  }
};


    // Place button immediately after response.
    root.insertAdjacentElement("afterend", button);

    root.dataset.normalMarkdownInstalled = "true";
    buttons.add(button);
  }

  function scan() {
    const roots = document.querySelectorAll(ROOT_SELECTOR);

    roots.forEach(inject);

    return roots.length;
  }

  const count = scan();

  // Watch for new responses.
  observer = new MutationObserver(() => {
    clearTimeout(scanTimer);
    scanTimer = setTimeout(scan, 400);
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true
  });

  window.__normalMarkdown = {
    scan,
    convert,
    stop() {
      observer.disconnect();
      clearTimeout(scanTimer);

      buttons.forEach(button => button.remove());
      buttons.clear();

      document.querySelectorAll(ROOT_SELECTOR)
        .forEach(root =>
          delete root.dataset.normalMarkdownInstalled
        );

      delete window.__normalMarkdown;

      console.log("[Normal Markdown] Stopped.");
    }
  };

  console.log(
    `[Normal Markdown] Installed on ${count} rendered responses.`
  );
})();

the updated version that also catches all URLs.

All code indents are removed to fit the length of the community message



/**
 * ChatGPT Classic Markdown Extractor
 *
 * @name ChatGPT Classic Markdown Extractor
 * @description Adds a "Copy normal Markdown" button to ChatGPT responses.
 * @author Scientific-Tools.Org
 * @version 1.1.0
 * @created 2026-10-12
 * @license MIT
 *
 * Converts rendered ChatGPT responses into standard Markdown (thus avoiding bad markdown recently introduced by the OpenAI UI update) and copies the result directly to the clipboard.
 * This JavaScript tool that adds a “Copy normal Markdown” button to all individual ChatGPT responses. It converts rendered answers into standard Markdown, preserving headings, bold and italic text, links, lists, tables, and code blocks. Clicking the button copies Markdown to the clipboard.
 * Enjoy the script and support Scientific-Tools.Org via https://scientific-tools.org/support-us/ if you find this script useful.
 * Note: This experimental script may not fully support interactive elements or Mermaid diagrams and can interfere with ChatGPT's interface.
 * If you need to remove the existing buttons and observer
 * window.__normalMarkdown.stop();
 * or 
 * window.__normalMarkdown?.stop?.()
 */


(() => {
 "use strict";

 window.__normalMarkdown?.stop?.();

 const VERSION = "1.1.0";
 const ROOT_SELECTOR = '[class*="DilResponseRoot-"]';
 const BUTTON_CLASS = "normal-markdown-copy-button";

 let observer = null;
 let scanTimer = null;

 const buttons = new Set();

 // ============================================================
 // 1. GET ORIGINAL DIL RESPONSE
 // ============================================================

 function getReference(root) {
 const fiberKey = Object.keys(root)
 .find(key => key.startsWith("__reactFiber$"));

 let fiber = fiberKey ? root[fiberKey] : null;

 while (fiber) {
 const ref = fiber.memoizedProps?.reference;

 if (ref?.type === "dil" && ref.model_dil_v2) {
 return ref;
 }

 fiber = fiber.return;
 }

 throw new Error(
 "Original cached DIL response not found."
 );
 }

 // ============================================================
 // 2. STATIC JAVASCRIPT EXPRESSION SCANNER
 //
 // Does not execute cached response code.
 // ============================================================

 function skipString(source, start) {
 const quote = source[start];
 let i = start + 1;

 while (i < source.length) {
 if (source[i] === "\\") {
 i += 2;
 } else if (source[i] === quote) {
 return i + 1;
 } else {
 i++;
 }
 }

 throw new Error("Unterminated string literal.");
 }

 function skipComment(source, start) {
 if (source.startsWith("//", start)) {
 const end = source.indexOf("\n", start + 2);
 return end < 0 ? source.length : end;
 }

 if (source.startsWith("/*", start)) {
 const end = source.indexOf("*/", start + 2);

 if (end < 0) {
 throw new Error("Unterminated comment.");
 }

 return end + 2;
 }

 return start;
 }

 function matchingEnd(source, start) {
 const pairs = {
 "(": ")",
 "[": "]",
 "{": "}"
 };

 const expected = pairs[source[start]];

 if (!expected) {
 throw new Error(
 "Expected opening delimiter at " + start
 );
 }

 const stack = [expected];

 for (let i = start + 1; i < source.length; i++) {
 const c = source[i];

 if (c === '"' || c === "'" || c === "`") {
 i = skipString(source, i) - 1;
 continue;
 }

 if (
 source.startsWith("//", i) ||
 source.startsWith("/*", i)
 ) {
 i = skipComment(source, i) - 1;
 continue;
 }

 if (pairs[c]) {
 stack.push(pairs[c]);
 continue;
 }

 if (c === ")" || c === "]" || c === "}") {
 if (stack.pop() !== c) {
 throw new Error(
 "Mismatched delimiters in cached DIL."
 );
 }

 if (!stack.length) {
 return i + 1;
 }
 }
 }

 throw new Error(
 "Unbalanced expression in cached DIL."
 );
 }

 function splitTopLevel(source, separator = ",") {
 const parts = [];
 let start = 0;

 for (let i = 0; i < source.length; i++) {
 const c = source[i];

 if (c === '"' || c === "'" || c === "`") {
 i = skipString(source, i) - 1;
 continue;
 }

 if (
 source.startsWith("//", i) ||
 source.startsWith("/*", i)
 ) {
 i = skipComment(source, i) - 1;
 continue;
 }

 if ("([{".includes(c)) {
 i = matchingEnd(source, i) - 1;
 continue;
 }

 if (c === separator) {
 parts.push(
 source.slice(start, i).trim()
 );

 start = i + 1;
 }
 }

 parts.push(source.slice(start).trim());

 return parts.filter(Boolean);
 }

 function findTopLevelColon(source) {
 for (let i = 0; i < source.length; i++) {
 const c = source[i];

 if (c === '"' || c === "'" || c === "`") {
 i = skipString(source, i) - 1;
 continue;
 }

 if ("([{".includes(c)) {
 i = matchingEnd(source, i) - 1;
 continue;
 }

 if (c === ":") {
 return i;
 }
 }

 return -1;
 }

 function decodeString(source) {
 const quote = source[0];

 if (quote === '"') {
 return JSON.parse(source);
 }

 if (quote === "'") {
 return source.slice(1, -1)
 .replace(/\\'/g, "'")
 .replace(/\\"/g, '"')
 .replace(/\\n/g, "\n")
 .replace(/\\r/g, "\r")
 .replace(/\\t/g, "\t")
 .replace(/\\\\/g, "\\");
 }

 if (quote === "`") {
 if (source.includes("${")) {
 throw new Error(
 "Dynamic template literals are unsupported."
 );
 }

 return source.slice(1, -1)
 .replace(/\\`/g, "`")
 .replace(/\\n/g, "\n")
 .replace(/\\t/g, "\t");
 }

 throw new Error(
 "Unsupported string literal."
 );
 }

 // ============================================================
 // 3. COMPILED DIL PARSER
 // ============================================================

 function parseDIL(code, constants = {}) {
 function parseExpression(input) {
 let source = String(input).trim();

 if (!source) {
 return null;
 }

 // Remove fully enclosing parentheses.
 while (
 source.startsWith("(") &&
 matchingEnd(source, 0) === source.length
 ) {
 source = source.slice(1, -1).trim();
 }

 if (!source) {
 return null;
 }

 // __dilSafe(() => (...), fallback)
 // Parse the callback's expression without executing it.
 if (source.startsWith("__dilSafe(")) {
 const open = source.indexOf("(");

 if (
 matchingEnd(source, open) !== source.length
 ) {
 throw new Error(
 "Unsupported __dilSafe expression."
 );
 }

 const args = splitTopLevel(
 source.slice(open + 1, -1)
 );

 const callback = args[0] || "";
 const arrow = callback.indexOf("=>");

 if (arrow < 0) {
 throw new Error(
 "Unsupported __dilSafe callback."
 );
 }

 return parseExpression(
 callback.slice(arrow + 2)
 );
 }

 // JSX calls:
 //
 // __dil.jsx(Component, props, ...children)
 //
 if (
 /^__dil\.(?:jsx|jsxs)\s*\(/.test(source)
 ) {
 const open = source.indexOf("(");

 if (
 matchingEnd(source, open) !== source.length
 ) {
 throw new Error(
 "Invalid compiled JSX expression."
 );
 }

 const args = splitTopLevel(
 source.slice(open + 1, -1)
 );

 if (!args.length) {
 throw new Error(
 "JSX call has no component type."
 );
 }

 const rawType = args[0];

 let type;

 if (/^["'`]/.test(rawType)) {
 type = decodeString(rawType);
 } else if (rawType === "__dil.Fragment") {
 type = "fragment";
 } else {
 type = rawType;
 }

 const props =
 args[1] && args[1] !== "null"
 ? parseExpression(args[1])
 : {};

 if (
 !props ||
 typeof props !== "object" ||
 Array.isArray(props)
 ) {
 throw new Error(
 "Invalid compiled JSX props."
 );
 }

 const positionalChildren = args.slice(2)
 .map(parseExpression);

 let children;

 if (positionalChildren.length === 1) {
 children = positionalChildren[0];
 } else if (positionalChildren.length > 1) {
 children = positionalChildren;
 } else {
 children = props.children ?? null;
 }

 return {
 __dilNode: true,
 type: String(type),
 props: {
 ...props,
 children
 }
 };
 }

 // DIL constants.
 const constantMatch = source.match(
 /^__dilConstants\[(["'])([^"']+)\1\]$/
 );

 if (constantMatch) {
 const key = constantMatch[2];

 if (
 !Object.prototype.hasOwnProperty.call(
 constants,
 key
 )
 ) {
 throw new Error(
 "Missing DIL constant: " + key
 );
 }

 return constants[key];
 }

 // Literal strings.
 if (/^["'`]/.test(source)) {
 if (
 skipString(source, 0) === source.length
 ) {
 return decodeString(source);
 }
 }

 if (
 source === "null" ||
 source === "undefined"
 ) {
 return null;
 }

 if (source === "true") {
 return true;
 }

 if (source === "false") {
 return false;
 }

 if (/^-?\d+(?:\.\d+)?$/.test(source)) {
 return Number(source);
 }

 // Arrays.
 if (
 source.startsWith("[") &&
 matchingEnd(source, 0) === source.length
 ) {
 return splitTopLevel(
 source.slice(1, -1)
 ).map(parseExpression);
 }

 // Objects.
 if (
 source.startsWith("{") &&
 matchingEnd(source, 0) === source.length
 ) {
 const result = {};

 for (const entry of splitTopLevel(
 source.slice(1, -1)
 )) {
 if (entry.startsWith("...")) {
 throw new Error(
 "Object spread is unsupported."
 );
 }

 const colon = findTopLevelColon(entry);

 if (colon < 0) {
 throw new Error(
 "Unsupported object property: " +
 entry.slice(0, 100)
 );
 }

 let key = entry.slice(0, colon).trim();

 if (/^["'`]/.test(key)) {
 key = decodeString(key);
 }

 result[key] = parseExpression(
 entry.slice(colon + 1)
 );
 }

 return result;
 }

 throw new Error(
 "Unsupported DIL expression: " +
 source.slice(0, 160)
 );
 }

 // The compiled source normally contains a wrapper:
 //
 // () => {
 // const __dilConstants = DIL.useConstants();
 // ...
 // return __dil.jsx(...);
 // }
 //
 // Extract the JSX expression after return.
 // Do not parse the entire wrapper as a component.

 const returnPattern =
 /\breturn\s+(?=__dil\.(?:jsx|jsxs)\s*\()/g;

 let match;
 let lastMatch = null;

 while (
 (match = returnPattern.exec(code))
 ) {
 lastMatch = match;
 }

 let start;

 if (lastMatch) {
 start = lastMatch.index +
 lastMatch[0].length;
 } else {
 const jsxMatch =
 /__dil\.(?:jsx|jsxs)\s*\(/.exec(code);

 if (!jsxMatch) {
 throw new Error(
 "No JSX expression found in cached DIL."
 );
 }

 start = jsxMatch.index;
 }

 const open = code.indexOf("(", start);
 const end = matchingEnd(code, open);

 const expression = code.slice(start, end);

 return [parseExpression(expression)];
 }

 // ============================================================
 // 4. SOURCE URL RESOLUTION
 // ============================================================

 function createCitationResolver(reference) {
 const model = reference.model_dil_v2;

 const results =
 model.appData?.opGenui?.componentResults || {};

 const safeURLs = new Set(
 reference.source_safe_urls || []
 );

 function extractURLs(value, seen = new WeakSet()) {
 if (typeof value === "string") {
 const urls =
 value.match(/https?:\/\/[^\s"'<>\\]+/g) || [];

 return urls.filter(url =>
 safeURLs.has(url)
 );
 }

 if (
 value === null ||
 typeof value !== "object" ||
 seen.has(value)
 ) {
 return [];
 }

 seen.add(value);

 return Object.values(value).flatMap(
 child => extractURLs(child, seen)
 );
 }

 function isArticleURL(url) {
 try {
 const parsed = new URL(url);

 if (
 !["http:", "https:"].includes(
 parsed.protocol
 )
 ) {
 return false;
 }

 if (
 /^(images?|static|cdn)\./i.test(
 parsed.hostname
 )
 ) {
 return false;
 }

 if (
 /\.(?:png|jpe?g|gif|webp|svg|ico|avif)(?:$|[?#])/i
 .test(url)
 ) {
 return false;
 }

 return true;
 } catch {
 return false;
 }
 }

 return function resolveCitation(node) {
 const resolutionId =
 node.props?.__resolutionId;

 if (!resolutionId) {
 throw new Error(
 "Citation has no __resolutionId."
 );
 }

 if (
 !Object.prototype.hasOwnProperty.call(
 results,
 resolutionId
 )
 ) {
 throw new Error(
 "Missing citation result: " +
 resolutionId
 );
 }

 const urls = [
 ...new Set(
 extractURLs(results[resolutionId])
 )
 ].filter(isArticleURL);

 if (!urls.length) {
 throw new Error(
 "No article URLs in citation result: " +
 resolutionId
 );
 }

 return urls;
 };
 }

 // ============================================================
 // 5. MARKDOWN UTILITIES
 // ============================================================

 function escapeMD(value) {
 return String(value)
 .replace(/\\/g, "\\\\")
 .replace(/([*_`[\]])/g, "\\$1");
 }

 function wrap(value, marker) {
 const text = String(value);

 if (!text.trim()) {
 return text;
 }

 const before = text.match(/^\s*/)[0];
 const after = text.match(/\s*$/)[0];

 const middle = text.slice(
 before.length,
 text.length - after.length
 );

 return (
 before +
 marker +
 middle +
 marker +
 after
 );
 }

 function mdURL(url) {
 return String(url)
 .replace(/\(/g, "%28")
 .replace(/\)/g, "%29");
 }

 function codeSpan(value) {
 const text = String(value);

 const runs = [...text.matchAll(/`+/g)]
 .map(match => match[0].length);

 const fence = "`".repeat(
 Math.max(
 1,
 ...runs.map(length => length + 1)
 )
 );

 const needsPadding =
 text.startsWith("`") ||
 text.endsWith("`") ||
 (
 text.startsWith(" ") &&
 text.endsWith(" ")
 );

 return (
 fence +
 (needsPadding ? " " : "") +
 text +
 (needsPadding ? " " : "") +
 fence
 );
 }

 function fencedCode(value, language = "") {
 const source = String(value)
 .replace(/\n$/, "");

 const runs = [...source.matchAll(/`+/g)]
 .map(match => match[0].length);

 const fence = "`".repeat(
 Math.max(
 3,
 ...runs.map(length => length + 1)
 )
 );

 return (
 fence +
 language +
 "\n" +
 source +
 "\n" +
 fence
 );
 }

 function normalizeType(type) {
 return String(type)
 .replace(/^__dil\./, "")
 .replace(/^.*\./, "")
 .toLowerCase();
 }

 function asArray(value) {
 if (value == null) {
 return [];
 }

 return Array.isArray(value)
 ? value
 : [value];
 }

 // ============================================================
 // 6. DIL -> MARKDOWN RENDERER
 // ============================================================

 function renderDIL(nodes, reference) {
 const resolveCitation =
 createCitationResolver(reference);

 const stats = {
 citations: 0,
 citationURLs: 0,
 fileCitations: 0
 };

 function childrenOf(node) {
 return node?.props?.children ?? null;
 }

 function plainText(value) {
 if (
 value == null ||
 typeof value === "boolean"
 ) {
 return "";
 }

 if (
 typeof value === "string" ||
 typeof value === "number"
 ) {
 return String(value);
 }

 if (Array.isArray(value)) {
 return value.map(plainText).join("");
 }

 if (value.__dilNode) {
 const type = normalizeType(value.type);

 if (type === "br") {
 return "\n";
 }

 if (
 type === "icon" ||
 type === "favicon"
 ) {
 return "";
 }

 return plainText(childrenOf(value));
 }

 throw new Error(
 "Unsupported DIL text value."
 );
 }

 function citationLabel(node, urls) {
 const props = node.props || {};

 const label =
 props.label ||
 props.value ||
 props.title;

 if (
 typeof label === "string" &&
 label.trim()
 ) {
 return label.trim();
 }

 try {
 return new URL(urls[0])
 .hostname
 .replace(/^www\./, "");
 } catch {
 return "Source";
 }
 }

 function renderCitation(node) {
 const urls = resolveCitation(node);

 stats.citations++;
 stats.citationURLs += urls.length;

 const label = citationLabel(node, urls);

 return urls.map((url, index) => {
 let name = label;

 if (index > 0) {
 try {
 name = new URL(url)
 .hostname
 .replace(/^www\./, "");
 } catch {
 name = `Source ${index + 1}`;
 }
 }

 return (
 `[${escapeMD(name)}]` +
 `(${mdURL(url)})`
 );
 }).join(" ");
 }

 function renderFileCitation(node) {
 stats.fileCitations++;

 const props = node.props || {};
 const ref = String(props.ref || "");

 const start = props.line_range_start;
 const end = props.line_range_end;

 let label = "File citation: " + ref;

 if (start != null) {
 label += " lines " + start;

 if (end != null && end !== start) {
 label += "-" + end;
 }
 }

 return `[${escapeMD(label)}]`;
 }

 function inline(value) {
 if (
 value == null ||
 typeof value === "boolean"
 ) {
 return "";
 }

 if (
 typeof value === "string" ||
 typeof value === "number"
 ) {
 return escapeMD(value);
 }

 if (Array.isArray(value)) {
 return value.map(inline).join("");
 }

 if (!value.__dilNode) {
 throw new Error(
 "Unsupported inline DIL value."
 );
 }

 const type = normalizeType(value.type);
 const props = value.props || {};
 const children = childrenOf(value);

		// Entity: preserve its visible name as Markdown text.
		// Entity is not an ordinary URL link and may not
		// contain a directly usable destination URL.
		if (type === "entity") {
		 const p = value.props || {};

		 const label =
			typeof p.value === "string" ? p.value :
			typeof p.query === "string" ? p.query :
			typeof p.ref === "string" ? p.ref :
			"";

		 return label
			? escapeMD(label)
			: inline(children);
		}

 if (type === "cite") {
 return renderCitation(value);
 }

 if (type === "filecite") {
 return renderFileCitation(value);
 }

 if (type === "br") {
 return "\n";
 }

 if (
 type === "icon" ||
 type === "favicon"
 ) {
 return "";
 }

 if (type === "code") {
 return codeSpan(plainText(children));
 }

 if (
 type === "strong" ||
 type === "b" ||
 type === "bold"
 ) {
 return wrap(inline(children), "**");
 }

 if (
 type === "em" ||
 type === "i" ||
 type === "italic"
 ) {
 return wrap(inline(children), "*");
 }

 if (
 type === "del" ||
 type === "s" ||
 type === "strikethrough"
 ) {
 return wrap(inline(children), "~~");
 }

 if (type === "underline") {
 return (
 "<u>" +
 inline(children) +
 "</u>"
 );
 }

 if (
 type === "a" ||
 type === "link"
 ) {
 const href =
 props.href ||
 props.url?.uri ||
 props.url;

 if (
 typeof href !== "string" ||
 !href
 ) {
 return inline(children);
 }

 const label =
 inline(children).trim() ||
 escapeMD(props.title || href);

 return (
 `[${label}]` +
 `(${mdURL(href)})`
 );
 }

 if (
 type === "img" ||
 type === "asyncimage"
 ) {
 const src =
 props.src ||
 props.url;

 if (typeof src !== "string") {
 return "";
 }

 return (
 `![${escapeMD(props.alt || "")}]` +
 `(${mdURL(src)})`
 );
 }

 if (type === "math") {
 if (typeof props.value !== "string") {
 throw new Error(
 "Dynamic math is unsupported."
 );
 }

 return props.block
 ? "\n$$\n" + props.value + "\n$$\n"
 : "$" + props.value + "$";
 }

 if (type === "text") {
 let result = inline(children);

 const bold =
 props.strong === true ||
 props.weight === "bold" ||
 props.weight === 700 ||
 props.weight === 600;

 const italic =
 props.italic === true;

 if (bold && italic) {
 result = wrap(result, "***");
 } else if (bold) {
 result = wrap(result, "**");
 } else if (italic) {
 result = wrap(result, "*");
 }

 if (props.lineThrough) {
 result = wrap(result, "~~");
 }

 if (props.underline) {
 result = "<u>" + result + "</u>";
 }

 return result;
 }

 if (type === "title") {
 return inline(children);
 }

 // Inline layout components.
 if ([
 "span",
 "label",
 "caption",
 "badge",
 "row",
 "col",
 "box",
 "pressable",
 "popover",
 "popover-trigger",
 "popover-content",
 "fragment",
 "div",
 "section",
 "article",
 "flow-item",
 "grid-item",
 "carousel-item",
 "card"
 ].includes(type)) {
 return inline(children);
 }

 throw new Error(
 "Unsupported inline DIL component: " +
 value.type
 );
 }

 function renderList(node, depth = 0) {
 const props = node.props || {};

 const ordered =
 normalizeType(node.type) === "ol" ||
 props.marker === "number" ||
 props.marker === "decimal";

 let number = Number(props.start || 1);

 const items = asArray(childrenOf(node));

 return items.map(item => {
 if (!item?.__dilNode) {
 return (
 " ".repeat(depth) +
 "- " +
 inline(item)
 );
 }

 const type = normalizeType(item.type);

 if (
 type !== "li" &&
 type !== "list-item"
 ) {
 return (
 " ".repeat(depth) +
 "- " +
 inline(item)
 );
 }

 const itemProps = item.props || {};

 const children = asArray(
 childrenOf(item)
 );

 const nested = children.filter(child =>
 child?.__dilNode &&
 ["ul", "ol", "list"].includes(
 normalizeType(child.type)
 )
 );

 const content = children.filter(
 child => !nested.includes(child)
 );

 let text = content.map(inline)
 .join("")
 .trim();

 if (
 !text &&
 typeof itemProps.label === "string"
 ) {
 text = escapeMD(itemProps.label);
 }

 let prefix;

 if (
 typeof itemProps.checked === "boolean"
 ) {
 prefix =
 itemProps.checked
 ? "- [x] "
 : "- [ ] ";
 } else {
 prefix = ordered
 ? `${number++}. `
 : "- ";
 }

 const line =
 " ".repeat(depth) +
 prefix +
 text;

 const sublists = nested.map(child =>
 renderList(child, depth + 1)
 ).join("\n");

 return line +
 (sublists ? "\n" + sublists : "");
 }).join("\n");
 }

 function renderTable(node) {
 const rows = [];

 function walk(value) {
 if (Array.isArray(value)) {
 value.forEach(walk);
 return;
 }

 if (!value?.__dilNode) {
 return;
 }

 const type = normalizeType(value.type);

 if (
 type === "tr" ||
 type === "table-row"
 ) {
 const cells = asArray(
 childrenOf(value)
 )
 .filter(cell =>
 cell?.__dilNode &&
 [
 "td",
 "th",
 "table-cell"
 ].includes(
 normalizeType(cell.type)
 )
 )
 .map(cell =>
 inline(childrenOf(cell))
 .replace(/\|/g, "\\|")
 .replace(/\n/g, " ")
 .trim()
 );

 if (cells.length) {
 rows.push(cells);
 }

 return;
 }

 walk(childrenOf(value));
 }

 walk(childrenOf(node));

 if (!rows.length) {
 throw new Error(
 "DIL table has no parseable rows."
 );
 }

 const cols = Math.max(
 ...rows.map(row => row.length)
 );

 const format = cells =>
 "| " +
 Array.from(
 { length: cols },
 (_, index) => cells[index] || ""
 ).join(" | ") +
 " |";

 return [
 format(rows[0]),
 format(Array(cols).fill("---")),
 ...rows.slice(1).map(format)
 ].join("\n");
 }

 function block(value) {
 if (
 value == null ||
 typeof value === "boolean"
 ) {
 return "";
 }

 if (
 typeof value === "string" ||
 typeof value === "number"
 ) {
 return inline(value);
 }

 if (Array.isArray(value)) {
 return value
 .map(block)
 .filter(Boolean)
 .join("\n\n");
 }

 if (!value.__dilNode) {
 throw new Error(
 "Unsupported block DIL value."
 );
 }

 const type = normalizeType(value.type);
 const props = value.props || {};
 const children = childrenOf(value);

 if (/^h[1-6]$/.test(type)) {
 return (
 "#".repeat(Number(type[1])) +
 " " +
 inline(children).trim()
 );
 }

 if (type === "title") {
 return (
 "## " +
 inline(children).trim()
 );
 }

 if (type === "p") {
 return inline(children).trim();
 }

 if (
 type === "ul" ||
 type === "ol" ||
 type === "list"
 ) {
 return renderList(value);
 }

 if (type === "table") {
 return renderTable(value);
 }

 if (type === "blockquote") {
 return block(children)
 .split("\n")
 .map(line =>
 line ? "> " + line : ">"
 )
 .join("\n");
 }

 if (
 type === "hr" ||
 type === "divider"
 ) {
 return "---";
 }

 if (type === "pre") {
 return fencedCode(
 plainText(children),
 props.language || ""
 );
 }

 if (type === "codeblock") {
 return fencedCode(
 plainText(children),
 props.language || ""
 );
 }

 if (
 type === "math" &&
 props.block
 ) {
 if (
 typeof props.value !== "string"
 ) {
 throw new Error(
 "Dynamic block math is unsupported."
 );
 }

 return (
 "$$\n" +
 props.value +
 "\n$$"
 );
 }

 if ([
 "fragment",
 "div",
 "section",
 "article",
 "box",
 "col",
 "row",
 "card",
 "grid",
 "grid-item",
 "flow",
 "flow-item",
 "carousel",
 "carousel-item",
 "table-section"
 ].includes(type)) {
 return block(children);
 }

 if ([
 "text",
 "bold",
 "italic",
 "strong",
 "em",
 "a",
 "link",
 "cite",
 "filecite",
 "span",
 "label",
 "caption",
 "badge",
 "code",
 "img",
 "asyncimage"
 ].includes(type)) {
 return inline(value);
 }

 throw new Error(
 "Unsupported block DIL component: " +
 value.type
 );
 }

 const markdown = block(nodes)
 .replace(/\n{3,}/g, "\n\n")
 .trim();

 return {
 markdown,
 stats
 };
 }

 // ============================================================
 // 7. CONVERSION ENTRY POINT
 // ============================================================

 function convert(root) {
 const reference = getReference(root);
 const model = reference.model_dil_v2;

 if (typeof model.code !== "string") {
 throw new Error(
 "Cached response has no DIL source code."
 );
 }

 const nodes = parseDIL(
 model.code,
 model.constants || {}
 );

 const result = renderDIL(
 nodes,
 reference
 );

 if (!result.markdown) {
 throw new Error(
 "DIL parser produced empty Markdown."
 );
 }

 console.info(
 "[Normal Markdown] DIL conversion",
 {
 citations: result.stats.citations,
 citationURLs:
 result.stats.citationURLs,
 fileCitations:
 result.stats.fileCitations,
 markdownLength:
 result.markdown.length
 }
 );

 return result.markdown;
 }

 // ============================================================
 // 8. COPY BUTTON
 // ============================================================

 function inject(root) {
 if (
 root.dataset.normalMarkdownInstalled
 ) {
 return;
 }

 const button =
 document.createElement("button");

 button.className = BUTTON_CLASS;
 button.type = "button";

 button.textContent =
 "Copy normal Markdown";

 button.title =
 "ChatGPT Classic Markdown Extractor " +
 VERSION +
 "\nCreated by Scientific-Tools.Org";
	 
 button.style.cssText = [
 "display:inline-block",
 "margin:10px 0",
 "padding:8px 14px",
 "border:1px solid #999",
 "border-radius:8px",
 "background:#e9e9e9",
 "color:#111",
 "font:13px system-ui",
 "cursor:pointer",
 "position:relative",
 "z-index:10"
 ].join(";");

 button.onclick = async event => {
 event.preventDefault();
 event.stopPropagation();

 const previousText =
 button.textContent;

 try {
 const markdown = convert(root);

 await navigator.clipboard.writeText(
 markdown
 );

 button.textContent =
 "✓ Markdown copied!";

 } catch (error) {
 console.error(
 "[Normal Markdown] DIL parser failed:",
 error
 );

 button.textContent =
 "✗ DIL parsing failed";

 alert(
 "Markdown extraction failed:\n\n" +
 error.message +
 "\n\nNo DOM matching fallback was used."
 );
 }

 setTimeout(() => {
 if (button.isConnected) {
 button.textContent =
 previousText;
 }
 }, 2000);
 };

 root.insertAdjacentElement(
 "afterend",
 button
 );

 root.dataset.normalMarkdownInstalled =
 "true";

 buttons.add(button);
 }

 // ============================================================
 // 9. AUTOMATIC INSTALLATION
 // ============================================================

 function scan() {
 const roots = document.querySelectorAll(
 ROOT_SELECTOR
 );

 roots.forEach(inject);

 return roots.length;
 }

 const count = scan();

 observer = new MutationObserver(() => {
 clearTimeout(scanTimer);

 scanTimer = setTimeout(
 scan,
 400
 );
 });

 observer.observe(document.body, {
 childList: true,
 subtree: true
 });

 window.__normalMarkdown = {
 version: VERSION,
 scan,
 convert,
 getReference,
 parseDIL,
 renderDIL,

 inspect(root) {
 const reference = getReference(root);
 const model = reference.model_dil_v2;

 return {
 reference,
 model,
 nodes: parseDIL(
 model.code,
 model.constants || {}
 )
 };
 },

 stop() {
 observer.disconnect();
 clearTimeout(scanTimer);

 buttons.forEach(button =>
 button.remove()
 );

 buttons.clear();

 document.querySelectorAll(
 ROOT_SELECTOR
 ).forEach(root => {
 delete root.dataset
 .normalMarkdownInstalled;
 });

 delete window.__normalMarkdown;

 console.log(
 "[Normal Markdown] Stopped."
 );
 }
 };

 console.log(
 `[Normal Markdown v${VERSION}] ` +
 `Installed on ${count} responses. ` +
 "Using cached DIL source only."
 );
})();


Is this safe to use I’m nervous