PromptPress

From LLM to DOCX/MD/PDF in one click

Paste raw output from Gemini or any LLM, preview the rendered LaTeX, and download versions ready for decks or shareable docs.

Markdown + GFMLaTeX / KaTeXDocx & PDF export
MathKaTeX ready
OutputClean & export

LLM input

Supports Markdown (GFM), inline LaTeX $E=mc^2$ and blocks $$f(x)$$. No server dependency: everything happens in the browser.

Auto-fix for LLM output

Cleans indentation (which becomes code) and converts `\( ... \)` / `\[ ... \]` into `$...$` / `$$...$$`.

Preview ready

Live render

PromptPress style

Copied from an LLM, formatted to be readable and ready to export.

Sample content

  • Inline formulas: E=mc2E = mc^2, (pi r^2)
  • Math block: 0ex2dx=π2\int_0^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
  • Hybrid list
    1. Title + short abstract
    2. Table with units
    3. Final notes section

Code snippet

def softmax(logits):
    exps = [pow(2.71828, x) for x in logits]
    s = sum(exps)
    return [v/s for v in exps]

Table

VariableDescriptionValue
rrRadius4.24.2
AAArea55.4,m255.4,m^2

Tip: paste raw output from Gemini or your LLM here.

Ready to export

Quick start (Tampermonkey + ChatGPT copy)

  1. Install Tampermonkey in your browser.
  2. Create a new userscript and paste the script below.
  3. Open https://chatgpt.com, click "Copy clean", and paste here.
  4. Keep Auto-fix set to On for best results.
// ==UserScript==
// @name         ChatGPT Copy Clean (tables + math)
// @match        https://chatgpt.com/*
// @grant        GM_setClipboard
// ==/UserScript==
(function () {
  const stripZeroWidth = (text) =>
    text.replace(/[\u200B\u200C\u200D\u2060-\u2064\uFEFF\u00AD\u202A-\u202E\u2066-\u2069]/g, "");

  const escapePipes = (text) =>
    text.replace(/\|/g, (m, offset, str) =>
      offset > 0 && str[offset - 1] === "\\" ? "|" : "\\|",
    );

  const normalizeMatrixRows = (tex) => {
    if (!/\\begin\{(bmatrix|pmatrix|matrix|aligned|align\*?|cases|array)\}/.test(tex)) {
      return tex;
    }
    return tex.replace(/(?<!\\)\\\s*(?=[0-9+\-\s\]])/g, "\\\\");
  };

  const flattenMath = (tex) =>
    tex
      .replace(/\r\n?/g, "\n")
      .split("\n")
      .map((l) => l.trim())
      .filter(Boolean)
      .join(" ");

  const wrapMath = (tex, displayHint) => {
    let body = normalizeMatrixRows(tex);
    body = flattenMath(body);

    const wantsDisplay =
      displayHint ||
      /\\begin\{[^}]+\}/.test(body) ||
      /\\end\{[^}]+\}/.test(body);

    if (wantsDisplay) return `\n\n$$\n${body}\n$$\n\n`;
    return `$${body}$`;
  };

  const isNoise = (el) => {
    const tag = el.tagName.toLowerCase();
    if (tag === "button") return true;
    const aria = (el.getAttribute("aria-label") || "").toLowerCase();
    if (aria.includes("copy code") || aria.includes("copia codice")) return true;
    const testId = (el.getAttribute("data-testid") || "").toLowerCase();
    if (testId.includes("copy") && testId.includes("code")) return true;
    return false;
  };

  const isMathContainer = (el) => {
    const tag = el.tagName.toLowerCase();
    if (tag === "math") return true;
    if (tag === "mjx-container" || tag === "mjx-assistive-mml") return true;
    if (tag === "script" && /^math\/tex/i.test(el.getAttribute("type") || "")) return true;
    if (tag === "img" && el.getAttribute("alt")) return true;
    if (el.classList.contains("katex-display") || el.classList.contains("katex")) return true;
    if (el.classList.contains("MathJax")) return true;
    return false;
  };

  const getStyleValue = (el, prop) => {
    const style = (el.getAttribute("style") || "").toLowerCase();
    const match = style.match(new RegExp(`${prop}\\s*:\\s*([^;]+)`));
    return match ? match[1].trim() : "";
  };

  const hasClass = (el, re) => re.test((el.getAttribute("class") || "").toLowerCase());

  const isBoldLike = (el) => {
    const weight = getStyleValue(el, "font-weight");
    if (weight) {
      if (weight.includes("bold") || weight.includes("bolder")) return true;
      const num = Number.parseInt(weight, 10);
      if (Number.isFinite(num) && num >= 600) return true;
    }
    return hasClass(el, /(^|\s)(font-bold|font-semibold|fw-bold|bold)(\s|$)/);
  };

  const isItalicLike = (el) => {
    const style = getStyleValue(el, "font-style");
    if (style && /italic|oblique/.test(style)) return true;
    return hasClass(el, /(^|\s)(italic|font-italic|is-italic)(\s|$)/);
  };

  const extractTex = (el) => {
    const tag = el.tagName.toLowerCase();
    const ann = el.querySelector('annotation[encoding="application/x-tex"]');
    if (ann && ann.textContent) return ann.textContent.trim();
    if (tag === "script") return el.textContent || "";
    if (tag === "img") return el.getAttribute("alt") || "";
    return (el.textContent || "").trim();
  };

  const renderMath = (el) => {
    const tex = extractTex(el);
    if (!tex) return "";
    const display =
      el.classList.contains("katex-display") ||
      el.closest(".katex-display") ||
      el.getAttribute("display") === "block";
    return wrapMath(tex, display);
  };

  const formatListItem = (prefix, raw) => {
    const normalized = raw.replace(/\r\n?/g, "\n").trimEnd();
    const lines = normalized.split("\n");

    while (lines.length && !lines[0].trim()) lines.shift();
    while (lines.length && !lines[lines.length - 1].trim()) lines.pop();

    if (!lines.length) return prefix.trimEnd();

    const first = (lines.shift() || "").trim();
    if (!lines.length) return `${prefix}${first}`.trimEnd();

    const rest = lines
      .map((line) => (line.length ? `  ${line}` : ""))
      .join("\n");

    return `${prefix}${first}\n${rest}`.trimEnd();
  };

  const renderInline = (node, marks = {}) => {
    if (node.nodeType === Node.TEXT_NODE) {
      return stripZeroWidth(node.textContent || "");
    }
    if (node.nodeType !== Node.ELEMENT_NODE) return "";
    const el = node;

    if (isNoise(el)) return "";
    if (isMathContainer(el)) return renderMath(el);

    const tag = el.tagName.toLowerCase();
    if (tag === "br") return "\n";
    if (tag === "ul" || tag === "ol") return "";

    const bold = tag === "strong" || tag === "b" || isBoldLike(el);
    const italics = tag === "em" || tag === "i" || isItalicLike(el);
    const code = tag === "code";

    const nextMarks = {
      bold: marks.bold || bold,
      italics: marks.italics || italics,
      code: marks.code || code,
    };

    const content = Array.from(el.childNodes)
      .map((child) => renderInline(child, nextMarks))
      .join("");

    if (!content) return "";

    let out = content;
    if (code && !marks.code) out = "`" + out + "`";
    if (bold && !marks.bold && italics && !marks.italics) {
      out = `***${out}***`;
    } else {
      if (bold && !marks.bold) out = `**${out}**`;
      if (italics && !marks.italics) out = `*${out}*`;
    }

    return out;
  };

  const renderTable = (tableEl) => {
    const rows = Array.from(tableEl.querySelectorAll("tr"));
    if (!rows.length) return "";

    const headerRow = tableEl.querySelector("thead tr") || rows[0];
    const headerCells = Array.from(headerRow.querySelectorAll("th, td"));
    if (!headerCells.length) return "";

    const colCount = headerCells.length;
    const bodyRows =
      headerRow === rows[0] ? rows.slice(1) : rows.filter((r) => r !== headerRow);

    const renderCell = (cell) => {
      const raw = Array.from(cell.childNodes)
        .map((child) => renderInline(child))
        .join("");
      let text = raw
        .replace(/\n\s*\$\$\s*\n([\s\S]*?)\n\s*\$\$\s*\n/g, (_, math) => `$${String(math).trim()}$`)
        .replace(/\n+/g, " ")
        .trim();

      text = escapePipes(text);
      return text.length ? text : " ";
    };

    const buildRow = (cells) => {
      const values = [];
      for (let i = 0; i < colCount; i += 1) {
        values.push(cells[i] ? renderCell(cells[i]) : " ");
      }
      return `| ${values.join(" | ")} |`;
    };

    const headerLine = buildRow(headerCells);
    const separator = `| ${Array(colCount).fill("---").join(" | ")} |`;
    const bodyLines = bodyRows
      .map((row) => buildRow(Array.from(row.querySelectorAll("th, td"))))
      .join("\n");

    return `\n\n${headerLine}\n${separator}${bodyLines ? `\n${bodyLines}` : ""}\n\n`;
  };

  const renderBlock = (node) => {
    if (node.nodeType === Node.TEXT_NODE) {
      const text = stripZeroWidth(node.textContent || "");
      return text.trim().length ? text : "";
    }
    if (node.nodeType !== Node.ELEMENT_NODE) return "";
    const el = node;

    if (isNoise(el)) return "";
    if (isMathContainer(el)) return `${renderMath(el)}\n\n`;

    const tag = el.tagName.toLowerCase();

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

    if (tag === "pre") {
      const code = el.textContent || "";
      return `\n\n\`\`\`\n${code.replace(/\n$/, "")}\n\`\`\`\n\n`;
    }

    if (tag === "table") return renderTable(el);

    if (tag === "ul" || tag === "ol") {
      const ordered = tag === "ol";
      const items = Array.from(el.children).filter(
        (child) => child.tagName.toLowerCase() === "li",
      );

      const lines = items
        .map((li, idx) => {
          const prefix = ordered ? `${idx + 1}. ` : "- ";
          const body = renderInline(li).trimEnd();
          const nestedLists = Array.from(li.children).filter((c) =>
            ["ul", "ol"].includes(c.tagName.toLowerCase()),
          );
          const nestedText = nestedLists
            .map((nested) => renderBlock(nested).trimEnd())
            .filter(Boolean)
            .join("\n");

          const combined = nestedText ? `${body}\n\n${nestedText}` : body;
          return formatListItem(prefix, combined);
        })
        .join("\n");

      return `${lines}\n\n`;
    }

    if (tag === "li") {
      const content = renderInline(el);
      return `${formatListItem("- ", content)}\n`;
    }

    if (/^h[1-6]$/.test(tag)) {
      const level = Number.parseInt(tag.slice(1), 10);
      const hashes = "#".repeat(Math.max(1, Math.min(6, level)));
      const text = renderInline(el).replace(/\n+/g, " ").trim();
      return text ? `${hashes} ${text}\n\n` : "";
    }

    if (tag === "p") {
      const text = renderInline(el).trim();
      return text ? `${text}\n\n` : "";
    }

    if (tag === "div" || tag === "section" || tag === "article") {
      const content = Array.from(el.childNodes)
        .map((child) => renderBlock(child))
        .join("")
        .trim();
      return content ? `${content}\n\n` : "";
    }

    const content = Array.from(el.childNodes).map((child) => renderBlock(child)).join("");
    return content;
  };

  const htmlToMarkdown = (root) => {
    const output = Array.from(root.childNodes)
      .map((node) => renderBlock(node))
      .join("")
      .replace(/\r\n?/g, "\n")
      .replace(/[ \t]+\n/g, "\n")
      .replace(/\n{3,}/g, "\n\n")
      .trim();

    return output;
  };

  const addButtons = () => {
    document.querySelectorAll('[data-message-id]').forEach((msg) => {
      if (msg.querySelector('.copy-clean-btn')) return;
      const btn = document.createElement('button');
      btn.textContent = 'Copy clean';
      btn.className = 'copy-clean-btn';
      btn.style.marginLeft = '8px';
      btn.onclick = () => copyClean(msg);
      const toolbar = msg.querySelector('[data-testid="toolbox"]') || msg;
      toolbar.appendChild(btn);
    });
  };

  const copyClean = (msg) => {
    const html = msg.innerHTML;
    const div = document.createElement('div');
    div.innerHTML = html;

    const md = htmlToMarkdown(div);
    GM_setClipboard(md, 'text');
    alert('Copied clean');
  };

  setInterval(addButtons, 1000);
})();