- // ==UserScript==
- // @name StackOverflow Code Language Switcher
- // @namespace https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
- // @homepageURL https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
- // @supportURL https://github.com/Christopher-Hayes/stackoverflow-code-language-switcher
- // @description Adds a dropdown to code blocks in StackOverflow to switch the code language. Powered by GPT-3 AI.
- // @match https://stackoverflow.com/questions/*
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addValueChangeListener
- // @grant GM_setClipboard
- // @version 1.0
- // @author Chris Hayes
- // @license GPL3
- // ==/UserScript==
-
- class LangConvert {
- constructor() {
- this.key = GM_getValue("openai_key", "");
-
- if (!this.key || this.key === "your-openai-key-here") {
- GM_setValue("openai_key", "your-openai-key-here");
- console.error(
- '"openai_key" not set in violent monkey script. Please get an OpenAI key and set this config value to use the OpenAI API.'
- );
- }
-
- this.supportedLanguages = GM_getValue("supported_languages", [
- "javascript",
- "typescript",
- "python",
- "java",
- "bash",
- "css",
- "scss",
- "html",
- "c",
- "c++",
- "c#",
- "rust",
- "go",
- "kotlin",
- "php",
- "ruby",
- "liquid",
- "swift",
- "react",
- "vue",
- "svelte",
- "angular",
- ]);
-
- GM_setValue("supported_languages", this.supportedLanguages);
-
- this.previousConversions = {};
- }
-
- setKey(newKey) {
- this.key = newKey;
- }
- setSupportedLanguages(newLanguages) {
- this.supportedLanguages = newLanguages;
- }
-
- async makeGPT3Request(code, oldLang, newLang) {
- const url = "https://api.openai.com/v1/completions";
- const headers = {
- "Content-Type": "application/json",
- Authorization: `Bearer ${this.key}`,
- };
-
- // Uses Davinci 003 text-completion
- // code-completion and edit were slower and didn't work as well
- const body = {
- model: "text-davinci-003",
- prompt: `# Convert this${oldLang ? " from " + oldLang : ""} to ${newLang}
- # ${oldLang ? oldLang : "Old"} version
-
- ${code}
-
- # ${newLang} version`,
- max_tokens: 1000,
- };
-
- const response = await fetch(url, {
- method: "POST",
- headers,
- body: JSON.stringify(body),
- });
- console.log(
- "[StackOverflow Code Language Switcher] Full OpenAI response:",
- response
- );
-
- const data = await response.json();
- console.log("[StackOverflow Code Language Switcher] Response data:", data);
-
- let newCode = data.choices[0].text;
- // if the first line is an empty newline, remove it
- if (newCode.startsWith("\n")) {
- newCode = newCode.slice(1);
- }
- // if the last line is an empty newline, remove it
- if (newCode.endsWith("\n")) {
- newCode = newCode.slice(0, -1);
- }
-
- return newCode;
- }
-
- getCodeElements() {
- return Array.from(document.querySelectorAll(".s-prose pre code"));
- }
-
- setup() {
- const codeElements = this.getCodeElements();
-
- for (const codeElem of codeElements) {
- const pre = codeElem.parentElement;
- const div = document.createElement("div");
- div.classList.add("lang-convert");
- div.style = `
- height: 0;
- float: right;
- margin-bottom: -20px;
- display: block;
- position: relative;
- top: 10px;
- right: 8px;
- `;
-
- // Show supported languages in dropdown
- const select = document.createElement("select");
- select.innerHTML = `${this.supportedLanguages
- .slice(0, 20)
- .map((lang) => `<option value="${lang}">${lang}</option>`)
- .join("")}`;
- select.style = `
- background: #181818;
- color: #999;
- outline: none;
- border: 0;
- padding: .5em 0.7em;
- border-radius: 7px;
- font-size: 12px;
- `;
- // Change style of select element when hovered over
- select.addEventListener("mouseenter", () => {
- select.style.background = "black";
- select.style.color = "white";
- });
- select.addEventListener("mouseleave", () => {
- select.style.background = "#181818";
- select.style.color = "#999";
- });
- div.appendChild(select);
-
- // Add div before pre element
- pre.parentElement.insertBefore(div, pre);
-
- // Set default value of dropdown to the language of the code element
- const lang = codeElem.className.split("-")[1];
- select.value = lang;
-
- this.previousConversions[lang] = codeElem.textContent;
-
- select.addEventListener("change", async (e) => {
- const oldLang = codeElem.className.split("-")[1];
- const newLang = e.target.value;
- if (oldLang === newLang) return;
-
- // Show "converting" overlay message
- const overlay = document.createElement("div");
- overlay.style = `
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- color: white;
- font-size: 20px;
- font-weight: bold;
- `;
- overlay.innerHTML = "Converting...";
- pre.style.position = "relative";
- pre.appendChild(overlay);
-
- const oldCode = codeElem.textContent;
- let newCode = "";
- if (this.previousConversions[newLang]) {
- newCode = this.previousConversions[newLang];
- } else {
- newCode = await this.makeGPT3Request(oldCode, oldLang, newLang);
- this.previousConversions[newLang] = newCode;
- }
-
- GM_setClipboard(newCode);
-
- codeElem.classList.remove(`language-${oldLang}`);
- codeElem.classList.add(`language-${newLang}`);
- codeElem.textContent = newCode;
-
- // add a script to the page that will highlight the new code using window.hljs
- // This must be injected to have access to the highlight.js object already loaded into StackOverflow
- const codeID =
- Math.random().toString(36).substring(2, 15) +
- Math.random().toString(36).substring(2, 15);
- codeElem.id = codeID;
- const script = document.createElement("script");
- script.innerHTML = `if (window.hljs) window.hljs.highlightElement(document.getElementById("${codeID}"));`;
- document.body.appendChild(script);
-
- // Remove "converting" overlay message
- pre.removeChild(overlay);
- pre.style.position = "static";
- });
- }
- }
- }
-
- let langConvert = new LangConvert();
- langConvert.setup();
-
- GM_addValueChangeListener("openai_key", (name, oldValue, newValue) => {
- langConvert.setKey(newValue);
- });
-
- GM_addValueChangeListener("supported_languages", (name, oldValue, newValue) => {
- langConvert.setSupportedLanguages(newValue);
- });