Last active
February 23, 2025 05:04
-
-
Save alexchexes/273ad5fa78a018f00ad2aeb7f9494a5c to your computer and use it in GitHub Desktop.
ChatGPT improved syntax highlighting (with support for Vue.js code)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name ChatGPT Better Syntax Highlighting | |
// @namespace http://tampermonkey.net/ | |
// @version 2024-12-01 | |
// @updateURL https://gist.github.com/alexchexes/273ad5fa78a018f00ad2aeb7f9494a5c/raw/chatgpt-better-syntax-highlighting.user.js | |
// @downloadURL https://gist.github.com/alexchexes/273ad5fa78a018f00ad2aeb7f9494a5c/raw/chatgpt-better-syntax-highlighting.user.js | |
// @description Automatically highlights unhighlighted code blocks with auto language recognition (via highlightjs). Handles chat switching, applies syntax highlighting to new messages on user interaction ("send" click or Enter keypress). Allows to re-highlight the block by clicking on its title. | |
// @author alexchexes | |
// @match https://chat.openai.com/* | |
// @match https://chatgpt.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com | |
// @require https://unpkg.com/@highlightjs/[email protected]/highlight.min.js | |
// @grant GM_addStyle | |
// ==/UserScript== | |
/* global hljs */ | |
(function() { | |
/*-------------------------------------------------------------* | |
* Imporoved Syntax Highlighting for Dark Theme * | |
* Manually adjusted base16-hardcore theme from @highlightjs * | |
* YOU CAN CHANGE THIS AS YOU WANT, OR REPLACE WITH OTHER THEME * | |
* As for now this is only for DARK chatGPT theme * | |
*--------------------------------------------------------------*/ | |
let darkThemeCss = ` | |
.hljs-comment { | |
color: #888 !important; | |
} | |
.hljs-tag { | |
color: #aaa !important; | |
} | |
.hljs-operator, | |
.hljs-punctuation, | |
.hljs-subst { | |
color: #cdcdcd !important; | |
} | |
.hljs-operator { | |
opacity: 0.7 !important; | |
} | |
.hljs-bullet, | |
.hljs-deletion, | |
.hljs-name, | |
.hljs-template-variable, | |
.hljs-variable { | |
color: #e8b882 !important; | |
} | |
.hljs-tag > .hljs-name { | |
color: #FF649C !important; | |
} | |
.hljs-tag > .hljs-attr { | |
color: #9DCF73 !important; | |
} | |
.hljs-tag > .hljs-string { | |
color: #e7ca72 !important; | |
} | |
.hljs-selector-tag { | |
color: #FF347F !important; | |
} | |
.hljs-selector-class { | |
color: #DFA768 !important; | |
} | |
.hljs-attr { | |
color: #D3D3D3 !important; | |
} | |
.hljs-link, | |
.hljs-literal, | |
.hljs-symbol, | |
.hljs-variable.constant_ { | |
color: #a785ec !important; | |
} | |
.hljs-number { | |
color: #8EF8B1 !important; | |
} | |
.hljs-class .hljs-title, | |
.hljs-title, | |
.hljs-title.class_ { | |
color: #A6E22E !important; | |
} | |
.hljs-strong { | |
font-weight: 700 !important; | |
color: #A6E22E !important; | |
} | |
.hljs-addition, | |
.hljs-code, | |
.hljs-string, | |
.hljs-title.class_.inherited__ { | |
color: #e6db74 !important; | |
} | |
.hljs-doctag, | |
.hljs-keyword.hljs-atrule, | |
.hljs-quote, | |
.hljs-regexp { | |
color: #9ac2ca !important; | |
} | |
.hljs-built_in { | |
color: #66D9EF !important; | |
font-style: italic !important; | |
} | |
.hljs-attribute, | |
.hljs-function .hljs-title, | |
.hljs-section, | |
.hljs-title.function_, | |
.ruby .hljs-property { | |
color: #66d9ef !important; | |
} | |
.hljs-property { | |
color: #ffe4d3 !important; | |
} | |
.hljs-function { | |
color: #66d9ef !important; | |
} | |
.diff .hljs-meta, | |
.hljs-keyword, | |
.hljs-template-tag { | |
color: #ff3982 !important; | |
} | |
.hljs-type { | |
color: #9EF0FF !important; | |
font-style: italic !important; | |
} | |
.hljs-emphasis { | |
color: #ff3982 !important; | |
font-style: italic !important; | |
} | |
.hljs-meta, | |
.hljs-meta .hljs-keyword, | |
.hljs-meta .hljs-string { | |
color: #7E9BAC !important; | |
} | |
.hljs-meta .hljs-keyword, | |
.hljs-meta-keyword { | |
font-weight: 700 !important; | |
} | |
.hljs-params { | |
color: #FD971F !important; | |
font-style: italic !important; | |
} | |
` | |
const element = document.querySelector('html'); // Select the <html> element | |
if (!element.classList.contains('light')) { | |
GM_addStyle(darkThemeCss); | |
} | |
const __PRE_TITLE_SELECTOR__ = 'pre > div > div.flex.items-center.text-token-text-secondary.select-none'; | |
const __SEND_BTN_SELECTOR__ = '[aria-label="Send prompt"]'; | |
const __HISTORY_ITEM_SELECTOR__ = 'ol > li[data-testid*="history-item-"]'; | |
const additionalCss = ` | |
${__PRE_TITLE_SELECTOR__} { | |
cursor: pointer; | |
} | |
${__PRE_TITLE_SELECTOR__}:hover { | |
color: #f39c12; | |
} | |
.hljs_pre_in_user_message { | |
padding: 2px 7px; | |
border-radius: 11px; | |
font-size: 14px; | |
} | |
html.dark .hljs_pre_in_user_message { | |
background: #000000a1 !important; | |
} | |
html.light .hljs_pre_in_user_message { | |
background: #f9f9f9 !important; | |
} | |
`; | |
GM_addStyle(additionalCss); | |
hljs.configure({ | |
ignoreUnescapedHTML: true | |
}); | |
// Utility: Debounce function for better performance | |
const debounce = (func, delay) => { | |
let timer; | |
return (...args) => { | |
clearTimeout(timer); | |
timer = setTimeout(() => func(...args), delay); | |
}; | |
}; | |
// Function to highlight a single <pre> and its <code> | |
const highlightPreElement = (preElement, force = false, language = null, titleElement = null) => { | |
const codeElement = preElement.querySelector('code'); | |
if (!codeElement) return; | |
// Check if the code block already has hljs tokens (like hljs-string) and is not in force mode | |
if ( | |
!force && | |
codeElement.querySelector('[class*="hljs-"]') | |
) { | |
return; | |
} | |
// Skip already processed blocks unless force is true | |
if (!force && codeElement.hasAttribute('data-highlighted')) { | |
return; | |
} | |
// If the <code> tag is a direct child of the <pre> - apply class for code in user message | |
if (codeElement.parentElement === preElement) { | |
preElement.classList.add('hljs_pre_in_user_message'); | |
} | |
codeElement.removeAttribute('data-highlighted'); | |
// Remove existing language classes to allow auto-detection or apply a specific language | |
codeElement.className = codeElement.className | |
.split(' ') | |
.filter((cls) => !cls.startsWith('language-') && cls !== 'hljs') | |
.join(' '); | |
if (language) { | |
// if we passed language and title, it means we want to apply initial language. Set title accordingly | |
if (titleElement) { | |
titleElement.innerHTML = language; | |
} | |
// Ensure Highlight.js supports the language | |
if (hljs.getLanguage(language)) { | |
codeElement.classList.add(`language-${language}`); | |
} else { | |
console.warn(`Language '${language}' not supported by Highlight.js`); | |
return; | |
} | |
} | |
// Trigger Highlight.js auto-detection or language-specific highlighting | |
hljs.highlightElement(codeElement); | |
// if titleElement provided, change the language name displayed in it | |
if (titleElement) { | |
const appliedLanguage = codeElement.className.match(/language-([^\s]+)/)?.[1]; | |
if (appliedLanguage) { | |
titleElement.innerHTML = appliedLanguage; | |
} | |
} | |
// Mark this block as highlighted | |
codeElement.setAttribute('data-highlighted', 'true'); | |
}; | |
// Function to highlight all unhighlighted <pre> elements on the page | |
const highlightAllOnPage = () => { | |
document.querySelectorAll('pre').forEach((preElement) => { | |
highlightPreElement(preElement); | |
}); | |
}; | |
const handleTitleClick = (titleElement) => { | |
// Get the language name from the title attribute or text content | |
let language = null; | |
// check for saved initial language name | |
if (titleElement.hasAttribute('initial-language')) { | |
language = titleElement.getAttribute('initial-language'); | |
} else { | |
language = titleElement.textContent.trim(); | |
// save the current displayed lang name to attribute | |
titleElement.setAttribute('initial-language', language); | |
} | |
if (!language) { | |
console.warn('Language name not found in title text'); | |
return; | |
} | |
// Get the parent <pre> and apply the language | |
const preElement = titleElement.closest('pre'); | |
if (preElement) { | |
highlightPreElement(preElement, true, language, titleElement); | |
} | |
} | |
const handlePreClick = (preElement) => { | |
// Check if the <code> inside <pre> is already highlighted | |
const codeElement = preElement.querySelector('code'); | |
if (codeElement && codeElement.hasAttribute('data-highlighted')) { | |
return; // Prevent unnecessary processing | |
} | |
// Highlight the clicked <pre> and its code block | |
highlightPreElement(preElement); | |
// Highlight all other unhighlighted <pre> elements on the page | |
highlightAllOnPage(); | |
} | |
// Function to handle body clicks | |
const handleBodySingleClick = (event) => { | |
// don't handle double-clicks here | |
if (event.detail > 1) return | |
// don't handle clicks that are part of text selection process | |
if (document.getSelection().type === 'Range') return | |
const target = event.target; | |
// Clicks on codeblock title | |
const titleElement = target.closest(__PRE_TITLE_SELECTOR__); | |
if (titleElement) { | |
handleTitleClick(titleElement); | |
return; | |
} | |
// Clicks on <pre> (but not title inside pre) | |
const preElement = target.closest('pre'); | |
if (preElement) { | |
handlePreClick(preElement); | |
return; | |
} | |
// Handle chat selection clicks | |
if (target.closest(__HISTORY_ITEM_SELECTOR__)) { | |
delayedHighlightAllOnPage(); | |
return; | |
} | |
// Handle send button clicks | |
if (target.closest(__SEND_BTN_SELECTOR__)) { | |
delayedHighlightAllOnPage(); | |
return; | |
} | |
}; | |
// Function to handle double-clicks on <pre> elements | |
const handleBodyDoubleClick = (event) => { | |
const titleElement = event.target.closest(__PRE_TITLE_SELECTOR__); | |
if(!titleElement) { | |
return | |
} | |
const preElement = event.target.closest('pre'); | |
if (!preElement) return; | |
highlightPreElement(preElement, true, null, titleElement); // Force re-highlight | |
}; | |
// Function to handle actions with a delay (for dynamically loaded content) | |
const delayedHighlightAllOnPage = debounce(() => { | |
highlightAllOnPage(); | |
}, 2500); // Adjust timeout as needed | |
// Function to handle Enter key presses | |
const handleBodyEnterPress = (event) => { | |
if (event.key === 'Enter' && !event.shiftKey) { | |
delayedHighlightAllOnPage(); | |
} | |
}; | |
// Attach the click, double-click, and keydown event listeners to the body | |
document.body.addEventListener('click', handleBodySingleClick); | |
document.body.addEventListener('dblclick', handleBodyDoubleClick); // New double-click listener | |
document.body.addEventListener('keydown', handleBodyEnterPress); | |
// Initial pass: Delayed highlighting for initial dynamic content load | |
delayedHighlightAllOnPage(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Before/After (Vue JS example):
Before:

After:

Code blocks inside user message:
Before:

After:

Broken HTML highlighting is fixed:
Before:

After:

There's also an ability to re-highlight an incorrectly highlighted (by chatGPT) code block by clicking on the header of the code block. Try it out!