Skip to content

Instantly share code, notes, and snippets.

@alexchexes
Last active February 23, 2025 05:04
Show Gist options
  • Save alexchexes/273ad5fa78a018f00ad2aeb7f9494a5c to your computer and use it in GitHub Desktop.
Save alexchexes/273ad5fa78a018f00ad2aeb7f9494a5c to your computer and use it in GitHub Desktop.
ChatGPT improved syntax highlighting (with support for Vue.js code)
// ==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();
})();
@alexchexes
Copy link
Author

Before/After (Vue JS example):

Before:
chrome_2024-12-16--18-59-25--401

After:
chrome_2024-12-16--18-59-25--401

Code blocks inside user message:

Before:
chrome_2024-12-16--18-59-25--401

After:
chrome_2024-12-16--18-59-25--401

Broken HTML highlighting is fixed:

Before:
chrome_2024-12-16--18-59-25--401

After:
chrome_2024-12-16--18-59-25--401

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!

chrome_2024-12-16--18-59-25--401 chrome_2024-12-16--18-59-25--401

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment