ChatGPT Markdown Formatter with Syntax Highlighting

Format user input in ChatGPT with Markdown and Syntax Highlighting

  1. // ==UserScript==
  2. // @name ChatGPT Markdown Formatter with Syntax Highlighting
  3. // @version 0.2
  4. // @description Format user input in ChatGPT with Markdown and Syntax Highlighting
  5. // @author u/sarke1 (and gpt-4)
  6. // @license MIT
  7. // @match https://chat.openai.com/*
  8. // @run-at document-idle
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
  10. // @grant none
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.2/markdown-it.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js
  13. // @namespace https://greasyfork.org/users/1224048
  14. // ==/UserScript==
  15.  
  16. (async () => {
  17. 'use strict';
  18.  
  19. const debug = false;
  20.  
  21. const log = (...vars) => {
  22. if (!debug) {
  23. return;
  24. }
  25.  
  26. console.log(...vars);
  27. };
  28.  
  29. log("Script starting...");
  30.  
  31. log("Libraries loaded...");
  32.  
  33. const md = markdownit({
  34. highlight: function (str, lang) {
  35. let highlightedCode, detectedLang;
  36.  
  37. if (lang && hljs.getLanguage(lang)) {
  38. try {
  39. const result = hljs.highlight(str, { language: lang, ignoreIllegals: true });
  40. highlightedCode = result.value;
  41. detectedLang = lang;
  42. } catch (__) {}
  43. } else {
  44. const result = hljs.highlightAuto(str);
  45. highlightedCode = result.value;
  46. detectedLang = result.language;
  47. }
  48.  
  49. return `<pre><div class="bg-black rounded-md mb-4">` +
  50. `<div class="flex items-center relative text-gray-200 bg-gray-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md">` +
  51. `<span>${detectedLang || 'plaintext'}</span>` +
  52. //`<button class="flex ml-auto gap-2">Copy code</button>` +
  53. `</div>` +
  54. `<div class="p-4 overflow-y-auto"><code class="!whitespace-pre hljs language-${detectedLang || 'plaintext'}">` +
  55. highlightedCode +
  56. `</code></div></div></pre>`;
  57. }
  58. });
  59.  
  60. log("Markdown-it initialized...");
  61.  
  62. const formatMessages = (observer) => {
  63. log("Formatting messages...");
  64.  
  65. // Disconnect observer to prevent recursion
  66. observer.disconnect();
  67.  
  68. document.querySelectorAll('main > div[role=presentation] div[data-message-author-role="user"]').forEach(messageDiv => {
  69. log("Checking user message: ", messageDiv);
  70.  
  71. const parentElement = messageDiv.parentElement;
  72. if (!parentElement.hasAttribute('data-user-message-container')) {
  73. parentElement.setAttribute('data-user-message-container', ''); // Tag the parent
  74. }
  75.  
  76. if (messageDiv.hasAttribute('data-formatted')) {
  77. log("Message already formatted.");
  78. return;
  79. }
  80.  
  81. const unformattedDiv = messageDiv.querySelector('div[class=""]');
  82.  
  83. const originalText = unformattedDiv.textContent;
  84. log("Original Text: ", originalText);
  85.  
  86. const formattedText = md.render(originalText);
  87.  
  88. const formattedDiv = document.createElement('div');
  89. formattedDiv.className = 'formatted-message markdown prose w-full break-words dark:prose-invert light';
  90. formattedDiv.innerHTML = formattedText;
  91. formattedDiv.style.whiteSpace = 'normal';
  92.  
  93. unformattedDiv.style.display = 'none';
  94.  
  95. messageDiv.setAttribute('data-formatted', 'true'); // Mark message as formatted
  96. messageDiv.appendChild(formattedDiv);
  97.  
  98. log("Message formatted.");
  99. });
  100.  
  101. // Reconnect observer
  102. observer.observe(targetNode, config);
  103. };
  104.  
  105. const userMessageSelector = '[data-message-author-role="user"]';
  106. let currentMessageCount = document.querySelectorAll(userMessageSelector).length;
  107.  
  108. const observerCallback = (mutationsList, observer) => {
  109. log("Mutation observed...");
  110.  
  111. const newMessageCount = document.querySelectorAll(userMessageSelector).length;
  112. if (newMessageCount !== currentMessageCount) {
  113. log(`New count: ${newMessageCount}`);
  114. currentMessageCount = newMessageCount;
  115. formatMessages(observer);
  116. }
  117. };
  118.  
  119. // Initialize MutationObserver
  120. const targetNode = document.querySelector('main');
  121. log("Target node for MutationObserver: ", targetNode);
  122.  
  123. const config = { childList: true, subtree: true };
  124.  
  125. const observer = new MutationObserver(observerCallback);
  126. observer.observe(targetNode, config);
  127. })();