javascript:(function(){ /* Configuration variables. */ var instructionAlert = 1; /* <-- CONFIGURATION - 1 is on (alert), 0 is off (no alert) */ var useRunningFix = 1; /* <-- CONFIGURATION - 1 is on (find and replace), 0 is off */ var replaceAllRunFix = 1; /* <-- CONFIGURATION - 1 is on (replace all), 0 is off */ /* Optional variables. */ var pageTitle = "ChatGPT Bookmark"; /* <-- OPTIONAL - change title */ var pageHead = "ChatGPT Response"; /* <-- OPTIONAL - change head */ /* DOM variables. */ /* Copy button that starts the bookmark, matched from the clicked element. */ var copyButtonSelector = /* select copy button by attribute */ "[aria-label=\"Copy\"]"; /* Copy buttons inside these are skipped - code block and prompt copy buttons. */ var skipCopySelector = /* HOT-GLUE - likely to change */ "pre, .markdown, [data-turn=\"user\"]"; /* Conversation turn holding one response and its button row. */ var turnSelector = /* HOT-GLUE - likely to change */ "article, [data-turn], [data-testid^=\"conversation-turn\"]"; /* Answer area in the turn - first selector with a match is used. */ var answerAreaSelector = [ /* HOT-GLUE - likely to change */ "[data-message-author-role=\"assistant\"] .markdown", ".markdown", "[data-message-author-role=\"assistant\"]" ]; /* Code block marked by the page, such as code with a Run button. */ var codeBlockSelector = /* HOT-GLUE - likely to change */ "[data-code-block], [data-markdown-copy=\"code-block\"]"; /* Code text in a block - code editor lines first, then code, then pre. */ var codeTextSelector = [".cm-content", "code", "pre"]; /* HOT-GLUE */ /* Code block - parents climbed from code to the whole block with its toolbar. */ var codeBlockClimbTags = ["DIV", "PRE", "SPAN"]; /* HOT-GLUE - likely to change */ var codeBlockToolbarLimit = 60; /* most characters of toolbar text in a block */ /* Switch variables. */ var onOffPasteReadyBookmarkResponse = 1; /* 1 (default) on, 0 is off */ /********************************************************************************** NOTE - 'HOT-GLUE' comments for fast; ok this works, and/or good enough patch, or DOM element that changes frequently. NOTE - use embedded object 'RUNNING_FIX', following syntax as illustrated below. RUNNING_FIX ILLUSTRATION: ************************* NOTE - this is mainly intended for code examples given by chatGPT. NOTE - if config is 1 (on), and no value or object undefined, bookmarklet still works. For items that transcripted unexpectedly in code boxes. TO USE - Follow pattern: "1": {one: "replace what", two: "with this"}, "2": {one: "replace what", two: "with this"}, etc... For each numeric nested object; property one is replaced with property two. Object is utilized in support funciton "runningFixChatGPTPasteReadyBookmark()". ***********************************************************************************/ /*********** RUNNING_FIX EXAMPLE: ******************** Best to keep a running object of this. Based upon answer where code for C++ was written as ` #include using namespace std;int main() { ` after being bookmarked using bookmarklet. Here a question was asked were several code examples in several languages were given, but for some reason C++ did not transpose correctly, NOTE - code blocks are now copied as plain text and skip RUNNING_FIX, so the C++ items are gone; add items here for text outside of code blocks. ***********/ var RUNNING_FIX = { "1": { one: "\\", /* IMPORTANT - this needs to stay */ two: "\" /* IMPORTANT - this needs to stay */ } }; /* Style the bookmarked page. */ { var bookmarkPageStyle = ` `; } /******************************** SUPPORT FUNCTIONS ********************************/ /* Add click event to document, running bookmark when a response copy button is clicked. */ const docEventChatGPTPasteReadyBookmark = () => { /* add a click event listener to the document */ document.addEventListener('click', function(event) { /* ensure bookmarklet is on and an error was not found */ if (onOffPasteReadyBookmarkResponse != 1) return; /* defined at top */ /* clicked element can be the button, its svg, or the svg path */ let clickedItem = event.target; if (!clickedItem || !clickedItem.closest) return; /* not an element */ /* select the copy button holding the clicked element */ let copyButton = clickedItem.closest(copyButtonSelector); /* run if copy button is not a code block or prompt copy button */ if (copyButton && !copyButton.closest(skipCopySelector)) { /* call main function */ pasteReadyBookmarkResponse(copyButton); } }); }; /* Find and replace items in object "RUNNING_FIX". */ const runningFixChatGPTPasteReadyBookmark = (curContent) => { /* ensure that syntax of object is followed */ let followingSyntax = 0; /* turns on if first is good, assumes remaining syntax */ if (RUNNING_FIX && /* if object is declared */ /* and is defined */ (RUNNING_FIX != undefined) || (RUNNING_FIX != null) ) { for (i in RUNNING_FIX) { /* check if first nested object follows syntax */ if (i == 1) { followingSyntax = 1; break; } else { /* end after one */ break; } } /* if syntax passed, continue; assuming remaining nested objects pass */ if (followingSyntax == 1) { for (i in RUNNING_FIX) { /* use a marking value with an unlikely string for an answer */ let unlikelyValue = "unlikely_v_a_l_u_e_for_a_random_book_m-a-r-k-l-e-t_RUNNING_FIX_un_like_ly"; /* check if replace all is turned on */ if (replaceAllRunFix == 1) { let gotAll = 0; /* variable 0 until all marked for replacement */ while (gotAll == 0) { /* while items found to be replace, mark with unlikely value */ if (curContent.indexOf(RUNNING_FIX[i].one) > -1) { curContent = curContent.replace(RUNNING_FIX[i].one, unlikelyValue); } else { /* marked all to be replaced */ gotAll = 1; } } gotAll = 0; /* variable 0 until all marked for replacement */ while (gotAll == 0) { /* while items marked with unlikely value, replace them */ if (curContent.indexOf(unlikelyValue) > -1) { curContent = curContent.replace(unlikelyValue, RUNNING_FIX[i].two); } else { /* replaced all marked values */ gotAll = 1; } } } else { /* replace first occurence */ curContent = curContent.replace(RUNNING_FIX[i].one, RUNNING_FIX[i].two); } } } return curContent; } }; /* Find the answer area for the copy button that was clicked. */ const findAnswerAreaChatGPTPasteReadyBookmark = (copyButton) => { /* conversation turn holding the copy button */ let curTurn = copyButton.closest(turnSelector); /* use first answer area selector with a match in the turn */ if (curTurn) { for (let i = 0; i < answerAreaSelector.length; i++) { let curAnswer = curTurn.querySelectorAll(answerAreaSelector[i]); if (curAnswer.length > 0) { return Array.from(curAnswer); /* response can have more than one part */ } } } /* fallback - check the element before each parent of the copy button */ let buttonRow = copyButton.parentElement; for (let i = 0; i < 20 && buttonRow && buttonRow != curTurn; i++) { /* safety limit */ let answerRow = buttonRow.previousElementSibling; if (answerRow && answerRow.innerText.trim().length >= 100) { /* probably match */ return [answerRow]; } buttonRow = buttonRow.parentElement; /* check next top-level element */ } /* answer area not found */ return []; }; /* Plain text of code, from the code editor when the block has one. */ const codeTextChatGPTPasteReadyBookmark = (curCode) => { let curText = ""; if (curCode.classList.contains("cm-content")) { /* whole document from the editor, as long code may only draw lines in view */ let curRef = curCode.cmView || curCode.cmTile; let curView = curRef && (curRef.view || (curRef.rootView && curRef.rootView.view) || (curRef.root && curRef.root.view)); if (curView && curView.state && curView.state.doc) { curText = curView.state.doc.toString(); } else { /* one editor line per element */ curText = Array.from(curCode.querySelectorAll(".cm-line")) .map((curLine) => curLine.textContent).join("\n"); } } else { /* line breaks and indents as shown on the page */ curText = curCode.innerText || curCode.textContent; } /* no-break spaces to spaces, and no trailing line breaks */ return curText.split(String.fromCharCode(160)).join(" ").replace(/\n+$/, ""); }; /* Mark each code block in the answer, saving its language and plain text. */ const markCodeBlocksChatGPTPasteReadyBookmark = (curArea, codeBlocks) => { /* mark block so its copy is swapped for a token */ let markBlock = (curBlock, curCode) => { /* language from toolbar text, else from the code's language attribute or class */ let curLang = ""; let curWalker = document.createTreeWalker(curBlock, NodeFilter.SHOW_TEXT); while (!curLang && curWalker.nextNode()) { let curNode = curWalker.currentNode; if (curCode.contains(curNode) || curNode.parentElement.closest("button")) continue; curLang = curNode.textContent.trim().slice(0, 30); } if (!curLang) { let langMatch = /language-([\w#+.-]+)/.exec(curCode.className); curLang = curCode.getAttribute("data-language") || (langMatch ? langMatch[1] : ""); } curBlock.setAttribute("data-paste-ready-code", codeBlocks.length); codeBlocks.push({lang: curLang, text: codeTextChatGPTPasteReadyBookmark(curCode)}); }; /* blocks the page marks as code blocks, such as code with a Run button */ curArea.querySelectorAll(codeBlockSelector).forEach((curBlock) => { if (curBlock.closest("[data-paste-ready-code]")) return; /* already in a block */ let curCode = null; for (let i = 0; i < codeTextSelector.length && !curCode; i++) { curCode = curBlock.querySelector(codeTextSelector[i]); } if (curCode) markBlock(curBlock, curCode); }); /* count of code and pre elements, including the element */ let codeCount = (curEl) => curEl.querySelectorAll("code, pre").length + (curEl.matches("code, pre") ? 1 : 0); /* other code that is a block - in a pre, has a language, or more than one line */ let curCodes = Array.from(curArea.querySelectorAll("pre, code")).filter((curCode) => (curCode.tagName == "PRE" && !curCode.querySelector("code")) || (curCode.tagName == "CODE" && (curCode.closest("pre") || /language-/.test(curCode.className) || curCode.textContent.indexOf("\n") > -1)) ); curCodes.forEach((curCode) => { if (curCode.closest("[data-paste-ready-code]")) return; /* already in a block */ let codeLength = curCode.textContent.length; /* climb to the parent holding only this code and its toolbar */ let curBlock = curCode.closest("pre") || curCode; let curPar = curBlock.parentElement; while (curPar && curPar != curArea && codeBlockClimbTags.indexOf(curPar.tagName) > -1 && curPar.querySelectorAll("code, pre").length == codeCount(curBlock) && curPar.textContent.length - codeLength <= codeBlockToolbarLimit) { curBlock = curPar; curPar = curBlock.parentElement; } markBlock(curBlock, curCode); }); }; /* Write a code block as html, safe in the bookmarklet string and the URL field. */ const codeBlockHTMLChatGPTPasteReadyBookmark = (curBlock) => { let encode = (curText) => curText .replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'").replace(/\\/g, "\") .replace(/`/g, "`").replace(/%/g, "%").replace(/\t/g, " ") .replace(/\r/g, "").replace(/\n/g, "
"); return "
" + (curBlock.lang ? "
" + encode(curBlock.lang) + "
" : "") + "
" + encode(curBlock.text) + "
"; }; /*********************************************************************************** MAIN FUNCTION ***********************************************************************************/ function pasteReadyBookmarkResponse(aWhat) { /* select the answer area for the copy button that was clicked */ let answerArea = findAnswerAreaChatGPTPasteReadyBookmark(aWhat); /* dry alert - repair needed */ let dryAlert = () => { alert("REPAIR NEEDED:\n\n" + " Please report issue on GitHub repo\n" + " https://github.com/isocialPractice/bookmarklets" ); /* turn off bookmarklet */ onOffPasteReadyBookmarkResponse = 0; }; /* define function variables */ let copiedContent, codeBlocks = []; /* ensure that answer area has been found */ if (answerArea.length == 0) { /* answer area not found */ console.log(aWhat); /* alert repair is needed */ dryAlert(); /* exit function */ return; } else { /* found from function findAnswerAreaChatGPTPasteReadyBookmark */ /* now the answer's div elements should be extracted, code blocks as tokens */ copiedContent = answerArea.map((curArea) => { markCodeBlocksChatGPTPasteReadyBookmark(curArea, codeBlocks); let curClone = curArea.cloneNode(true); curClone.querySelectorAll("[data-paste-ready-code]").forEach((curBlock) => { curBlock.replaceWith( "codeblock_paste_ready_" + curBlock.getAttribute("data-paste-ready-code") + "_end" ); }); return curClone.innerHTML; }).join(""); /* remove code block marks from the page */ document.querySelectorAll("[data-paste-ready-code]").forEach((curBlock) => { curBlock.removeAttribute("data-paste-ready-code"); }); } /* clean up a bit */ copiedContent = copiedContent.replace(/\n/g, "--new-line_unlikely_line-new--"); /* new line marker replaced later */ copiedContent = copiedContent.replace(/\"/g, '\"'); /* use html encoding for escaped double quotes */ copiedContent = copiedContent.replace(/"/g, '\"'); /* keep double quotes as needed for inline html */ copiedContent = copiedContent.replace(/\'/g, "\''"); /* use html encoding for escaped single quotes */ copiedContent = copiedContent.replace(/'/g, "'"); /* keep single quotes as needed for inline html */ copiedContent = copiedContent.replace(/class=\"/g, "class='"); /* HOT-GLUE - correct open class - HOT-GLUE */ copiedContent = copiedContent.replace(/\">/g, "'>"); /* HOT-GLUE - correct close class - HOT-GLUE */ /* utilize "RUNNING_FIX" to correctly write unexpected outputs */ copiedContent = runningFixChatGPTPasteReadyBookmark(copiedContent); /* swap code block tokens for plain text code blocks */ copiedContent = copiedContent.replace(/codeblock_paste_ready_(\d+)_end/g, (curToken, curIndex) => codeBlockHTMLChatGPTPasteReadyBookmark(codeBlocks[curIndex]) ); /* alert to wait - if pasted immediately = does not work */ alert( "Wait 2 seconds for content to generate as bookmarklet on clipboard. \n" + "Then open bookmark manager and make new bookmark by pasting into url field" ); /* HOT-GLUE - replace new line characters only in pre tags with
*/ /* remove unlikely new line marker in remaining html */ var pageScript = ` `; /* store new bookmarklet to be written to clipboard */ var bookmarklet = 'javascript:(function() {document.write("' + /* write html head content */ "" + "" + pageTitle + "" + /* style a bit */ bookmarkPageStyle + /* add page body and div */ "
" + "

" + pageHead + "


" + /* sidebarlink to original answer */ "
" + "ChatGPT" + '

ChatGPT Bookmark

' + /* cleaned copied content */ copiedContent + /* close page div and add pageScript */ "
" + pageScript + /* close html */ "" + /* close bookmarklet */ '");})()'; /* HOT-GLUE - remove unwanted escaped double quotes - HOT-GLUE */ bookmarklet = bookmarklet.replace(/\"/g, """); /* HOT-GLUE - remove unwanted escaped single quotes - HOT-GLUE */ bookmarklet = bookmarklet.replace(/\'/g, ""); /* HOT-GLUE - remove bactic as probably md styling - HOT-GLUE */ bookmarklet = bookmarklet.replace(/`/g, ""); /* set timeout so bookmarklet content is copied after answers' text */ setTimeout(function () { navigator.clipboard.writeText(bookmarklet); }, 2000); } /* Run function that checks whenever a click event fires on page. */ docEventChatGPTPasteReadyBookmark(); /* If instruction alert is on. */ if (instructionAlert == 1) { alert( "INSTRUCTIONS: \n" + "**************** \n" + "IMPORTANT - click copy button around bottom. \n\n" + "1. Run bookmarklet. \n" + "2. Click the copy button below the answer. \n" + "3. Create a new bookmark, pasting contents of clipboard in the URL box."); } } )();