Created
November 17, 2024 02:47
-
-
Save doubleedesign/e6a22ff3252cbb3feb21a04ad2a37670 to your computer and use it in GitHub Desktop.
Customised Xdebug output styling
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
/** | |
* Customise some of the HTML output of xdebug + add highlightjs + hackily customise that | |
* Requires: | |
* - HighlightJS: https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js | |
* - HighlightJS PHP: https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/php.min.js | |
* - Optionally, a base theme, e.g., https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css | |
**/ | |
document.addEventListener('DOMContentLoaded', function () { | |
const codeBlocks = document.querySelectorAll('.xdebug-error td:nth-of-type(4)'); | |
if(codeBlocks.length) { | |
codeBlocks.forEach((codeBlock) => { | |
codeBlock.classList.add('php'); | |
// Add highlighting | |
// Note: This needs to happen before manipulating the content below, or else things break | |
hljs.highlightElement(codeBlock); | |
// Wrap truncated arrays and objects in a span | |
wrapTruncatedObjects(codeBlock); | |
// Add an extra class to string elements that are WordPress block output strings | |
wrapWpBlockHtml(codeBlock); | |
// Use a regular expression to match the word "class" followed by a class name | |
// and wrap the class name in a span with a class that matches what highlight.js uses for others | |
wrapKeywords(codeBlock); | |
// Detect and wrap arrays | |
if(codeBlock.innerText.includes('[') && codeBlock.innerText.includes(']')) { | |
wrapArrayContents(codeBlock); | |
} | |
// Detect classes and add a wrapping div to their inner contents | |
if(codeBlock.innerText.includes('{') && codeBlock.innerText.includes('}')) { | |
replaceFirstAndLastBraces(codeBlock); | |
} | |
// Detect function arguments using () and add a wrapping span to their inner contents | |
if(codeBlock.innerText.includes('(') && codeBlock.innerText.includes(')')) { | |
wrapFunctionArgs(codeBlock); | |
} | |
// Detect and wrap namespaces and file paths (done together because they both use backslashes) | |
if(codeBlock.innerText.includes('\\')) { | |
wrapNamespacesAndFilePaths(codeBlock); | |
} | |
}); | |
} | |
}); | |
function wrapTruncatedObjects(node) { | |
node.innerHTML = node.innerHTML | |
.replaceAll('= [...];', '<span class="hljs-array-truncated">= [...];</span>') | |
.replaceAll('= [...]', '<span class="hljs-array-truncated">= [...]</span>') | |
.replaceAll('=> { ... };', '<span class="hljs-class-truncated">=> { ... };</span>') | |
.replaceAll('{ ... };', '<span class="hljs-class-truncated">{ ... };</span>') | |
} | |
function wrapWpBlockHtml(node) { | |
const stringElements = node.querySelectorAll('.hljs-string'); | |
const blockElements = Array.from(stringElements).filter((el) => { | |
const content = el.textContent.trim(); | |
return content.startsWith("'<!-- wp:") && content.endsWith("-->'"); | |
}); | |
blockElements.forEach((el) => { | |
el.classList.add('hljs-wp-block'); | |
// Split the content into parts at \n and wrap each newline in a span | |
const parts = el.innerHTML.split(/\\n/); | |
const wrappedContent = parts.map(part => { | |
return `<span class="hljs-wp-block-line">${part}</span>`; | |
}).join(''); | |
el.innerHTML = wrappedContent; | |
}); | |
} | |
function wrapKeywords(node) { | |
const keywordSpans = node.querySelectorAll('span.hljs-keyword'); | |
keywordSpans.forEach((span) => { | |
if (span.textContent.trim() === 'class') { | |
// Update the class name of this span to identify it as a class keyword | |
span.classList.add('class_'); | |
// Traverse siblings until we find the class name as plain text | |
let sibling = span.nextSibling; | |
while (sibling && (sibling.nodeType !== node.TEXT_NODE || sibling.textContent.trim() === '')) { | |
sibling = sibling.nextSibling; | |
} | |
if (sibling && sibling.nodeType === node.TEXT_NODE) { | |
// Extract the class name using regex | |
const match = sibling.textContent.trim().match(/^([a-zA-Z0-9_]+)/); | |
if (match) { | |
const className = match[1]; | |
const wrappedClassName = `<span class="hljs-title class_">${className}</span>`; | |
const newHTML = sibling.textContent.replace(className, wrappedClassName); | |
// Create a temporary container to parse the HTML | |
const tempDiv = document.createElement('div'); | |
tempDiv.innerHTML = newHTML; | |
// Insert the parsed HTML back into the DOM and remove the original node | |
while (tempDiv.firstChild) { | |
sibling.parentNode.insertBefore(tempDiv.firstChild, sibling); | |
} | |
sibling.parentNode.removeChild(sibling); | |
} | |
} | |
} | |
}); | |
} | |
function wrapFunctionArgs(node) { | |
// Add an opening div after the opening ( | |
node.innerHTML = node.innerHTML.replace('(', '(<span class="hljs-function-args">'); | |
// Add a closing div before the closing ) | |
node.innerHTML = node.innerHTML.replace(')', '</span>)'); | |
} | |
function replaceFirstAndLastBraces(node) { | |
const htmlContent = node.innerHTML; | |
// Find the position of the first `{` and the last `}` | |
const firstBraceIndex = htmlContent.indexOf('{'); | |
const lastBraceIndex = htmlContent.lastIndexOf('}'); | |
if (firstBraceIndex !== -1 && lastBraceIndex !== -1) { | |
// Split the HTML content into three parts | |
const beforeFirstBrace = htmlContent.substring(0, firstBraceIndex); | |
const firstBrace = '{<div class="hljs-class-contents">'; | |
const betweenBraces = htmlContent.substring(firstBraceIndex + 1, lastBraceIndex); | |
const lastBrace = '</div>}'; | |
const afterLastBrace = htmlContent.substring(lastBraceIndex + 1); | |
// Reconstruct the HTML with the wrapping div | |
node.innerHTML = beforeFirstBrace + firstBrace + betweenBraces + lastBrace + afterLastBrace; | |
} | |
} | |
function wrapArrayContents(node) { | |
// Wrap [ ... ] and their inner contents, excluding [ ... ] | |
node.innerHTML = node.innerHTML.replace(/\[(?!\s*\.\.\.\s*\])([^\[\]]*?)\]/g, (match, innerContent) => { | |
// Split the inner content on commas | |
const parts = innerContent.split(','); | |
// Wrap each part in its own span | |
const wrappedParts = parts.map(part => `<span class="hljs-array-item">${part},</span>`); | |
// Rejoin the parts | |
const wrappedContentItems = wrappedParts.join(''); | |
return `<span class="hljs-array">[<div class="hljs-array-contents">${wrappedContentItems}</div>]</span>`; | |
}); | |
} | |
function wrapNamespacesAndFilePaths(node) { | |
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null); | |
let currentNode; | |
while ((currentNode = walker.nextNode())) { | |
// Skip text nodes that are children of .hljs-string spans | |
if (currentNode.parentNode.classList && currentNode.parentNode.classList.contains('hljs-string')) { | |
continue; | |
} | |
// Check if the text node contains a backslash | |
if (currentNode.nodeValue.includes('\\')) { | |
// Split the text content by `->` to separate parts | |
const parts = currentNode.nodeValue.split('->'); | |
// Create a document fragment to rebuild the node | |
const fragment = document.createDocumentFragment(); | |
parts.forEach((part, index) => { | |
if (part.includes('\\')) { | |
// Create a span for parts with backslashes | |
const span = document.createElement('span'); | |
if(currentNode.nodeValue.includes('C:\\')) { | |
span.className = 'hljs-filepath'; | |
} else { | |
span.className = 'hljs-namespace'; | |
} | |
span.textContent = part; | |
fragment.appendChild(span); | |
} | |
else if (part) { | |
// Add plain text for parts without backslashes | |
fragment.appendChild(document.createTextNode(part)); | |
} | |
// Add `->` back between parts, except after the last one | |
if (index < parts.length - 1) { | |
fragment.appendChild(document.createTextNode('->')); | |
} | |
}); | |
// Replace the original text node with the fragment | |
currentNode.parentNode.replaceChild(fragment, currentNode); | |
} | |
} | |
} |
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
<?php | |
/** | |
* Optional: Resolve Xdebug links to Jetbrains URLs so clicking them opens the file in PHPStorm | |
* To turn links on and tell them to use this file, add to php.ini: | |
* xdebug.file_link_format="http://YOUR_LOCAL_SITE_URL/xdebug-path-resolver.php?file=%f&line=%l" | |
*/ | |
if (isset($_GET['file'])) { | |
$fullPath = $_GET['file']; | |
$line = $_GET['line'] ?? null; | |
$projectName = str_replace('.local', '', $_SERVER['HTTP_HOST']); | |
$relativePathPieces = explode('\\', $fullPath); | |
// Implode all the pieces after $projectName | |
$index = array_search($projectName, $relativePathPieces); | |
$relativePath = implode('/', array_slice($relativePathPieces, $index + 1)); | |
// Construct the JetBrains URL | |
// Example: jetbrains://php-storm/navigate/reference?project=starterkit-playground-ii&path=render-bridge/components/Paragraph.php&line=5 | |
$url = "jetbrains://php-storm/navigate/reference?project=$projectName&path=$relativePath"; | |
// The line number doesn't work at the time of writing, but I'm leaving it in for future reference in case the functionality is added | |
if ($line) { | |
$url .= "&line=$line"; | |
} | |
// Redirect to JetBrains URL | |
header("Location: $url"); | |
exit; | |
} |
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
/** | |
* Note: Some of these styles rely on additional classes and wrapping elements that are added using JS | |
* (both using highlight.js and some custom code). | |
* Also, CSS ordering has been implemented to put the most useful stack trace messages at the top, | |
* but note that this makes keyboard navigation go backwards if you have links enabled. | |
**/ | |
:root { | |
--xdebug-link-color: #a275ad; | |
--xdebug-link-hover-color: #9bdeac; | |
--xdebug-warning-color: #ffb57a; | |
--xdebug-code-variable-color: #9b48de; | |
--xdebug-code-string-color: #5b5b5b; | |
--xdebug-code-keyword-color: #472bc4; | |
--xdebug-code-class-color: #0a9bbb; | |
--xdebug-code-function-color: #28b06b; | |
} | |
.xdebug-error { | |
font-family: Arial, Helvetica, sans-serif; | |
background: white; | |
max-width: 900px; | |
margin: 1rem auto; | |
border: 0; | |
font-size: 0.9rem; | |
a { | |
color: var(--xdebug-link-color) !important; | |
&:hover, &:focus, &:active { | |
color: var(--xdebug-link-hover-color) !important; | |
} | |
} | |
/* Reverse the order of the stack trace because it's more useful that way */ | |
/* But note this does make tab order the wrong way around if navigating via keyboard */ | |
tbody { | |
display: flex; | |
flex-direction: column-reverse; | |
/* Put the main error messages back at the top */ | |
tr:has(th[colspan="5"]) { | |
order: 10; | |
} | |
} | |
/* Error message block */ | |
th[colspan="5"] { | |
padding: 0.5rem; | |
display: block; | |
margin-block-end: 1rem; | |
background: white; | |
border: 1px solid var(--xdebug-warning-color); | |
border-left: 2rem solid var(--xdebug-warning-color); | |
font-weight: normal; | |
line-height: 1.4; | |
; | |
> span:first-child { | |
display: none; | |
} | |
a { | |
display: block; | |
} | |
} | |
/* Row that says "Call stack" */ | |
tr:has(th[bgcolor="#e9b96e"]) { | |
display: none; | |
} | |
/* Call stack column headers */ | |
tr:has(th:nth-child(5)) { | |
display: none; | |
} | |
/* Call stack rows */ | |
tr:has(td:nth-child(5)) { | |
display: grid; | |
grid-template-columns: 2rem repeat(5, 1fr); | |
grid-template-rows: repeat(3, auto); | |
grid-gap: 0; | |
margin-block-end: 1rem; | |
border: 1px solid #ccc; | |
td { | |
border: 0; | |
text-align: left; | |
background: transparent; | |
align-items: center; | |
padding: 0.5rem; | |
&:before { | |
font-family: Arial, Helvetica, sans-serif; | |
font-weight: bold; | |
display: inline-block; | |
} | |
&:nth-child(1) { | |
grid-area: 1 / 1 / 4 / 2; | |
background: #EDEDED; | |
} | |
&:nth-child(2), | |
&:nth-child(3) { | |
text-align: right; | |
font-size: 0.7rem; | |
} | |
&:nth-child(2) { | |
grid-area: 1 / 5; | |
&:before { | |
content: 'Time: '; | |
} | |
} | |
&:nth-child(3) { | |
grid-area: 1 / 6; | |
&:before { | |
content: 'Memory: '; | |
} | |
} | |
&:nth-child(4) { | |
grid-area: 2 / 2 / 3 / 7; | |
font-family: 'Fira Code', monospace; | |
line-height: 1.4; | |
border-bottom: 1px solid #DDD; | |
border-top: 1px solid #DDD; | |
margin-inline: 0.25rem; | |
&:before { | |
content: 'Function: '; | |
display: block; | |
margin-block-end: 0.25rem; | |
} | |
font { | |
display: inline-block; | |
max-width: 750px; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
direction: rtl; /* Reverse the overflow direction */ | |
vertical-align: middle; | |
margin-inline-start: -0.75rem; | |
margin-inline-end: -0.75rem; | |
} | |
} | |
&:nth-child(5) { | |
grid-area: 3 / 2 / 4 / 7; | |
display: flex; | |
align-items: center; | |
&:before { | |
content: 'Location: '; | |
display: block; | |
} | |
&:after { | |
content: attr(title); | |
margin-left: auto; | |
max-width: 600px; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
direction: rtl; /* Reverse the overflow direction */ | |
} | |
} | |
} | |
} | |
/* highlightjs syntax highlighting overrides */ | |
.hljs.language-php { | |
} | |
/** Colours **/ | |
.hljs-keyword { | |
color: var(--xdebug-code-keyword-color); | |
} | |
.hljs-title.class_, | |
.hljs-class .hljs-title, | |
.hljs-namespace { | |
color: var(--xdebug-code-class-color); | |
} | |
.hljs-variable { | |
color: var(--xdebug-code-variable-color); | |
} | |
.hljs-title.function_ { | |
color: var(--xdebug-code-function-color); | |
} | |
.hljs-string, | |
.hljs-array-truncated, | |
.hljs-class-truncated { | |
color: var(--xdebug-code-string-color); | |
} | |
/** Spacing and hacky line breaks **/ | |
.hljs-array { | |
} | |
.hljs-array-contents, | |
.hljs-class-contents { | |
margin-left: 2rem; | |
} | |
.hljs-array-item { | |
display: block; | |
} | |
/* Put args on the next line and indent them if there's 3 or more */ | |
.hljs-function-args { | |
&:not(:has(:first-child)) { /* :empty doesn't work because there's whitespace */ | |
display: none; | |
} | |
&:has(:nth-child(3)) { | |
display: block; | |
margin-left: 2rem; | |
> .hljs-variable { | |
&:before { | |
display: block; | |
margin-left: 1em; | |
height: 0; | |
width: 100%; | |
content: '\A'; /* Inserts a line break */ | |
white-space: pre; /* Ensures the line break is rendered */ | |
} | |
} | |
} | |
} | |
.hljs-class-contents > .hljs-keyword:not(.class_), | |
.hljs-array .hljs-variable { | |
&:before { | |
display: block; | |
height: 0; | |
width: 100%; | |
content: '\A'; /* Inserts a line break */ | |
white-space: pre; /* Ensures the line break is rendered */ | |
} | |
} | |
.hljs-title.function_ { | |
} | |
.hljs-wp-block { | |
display: block; | |
margin-left: 2rem; | |
.hljs-wp-block-line { | |
display: block; | |
} | |
.hljs-class-contents { | |
margin: 0; | |
display: contents; | |
} | |
} | |
/** Truncate long strings **/ | |
.hljs-string:not(.hljs-wp-block) { | |
display: inline-block; | |
max-width: 600px; | |
vertical-align: middle; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
} | |
} |
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
<?php | |
/** | |
* Enqueue styles and scripts to make xdebug output more readable for admins in local environments | |
* Note: WP_ENVIRONMENT_TYPE is a constant defined in wp-config.php | |
* | |
* @return void | |
*/ | |
function doublee_make_xdebug_pretty(): void { | |
if (defined('WP_ENVIRONMENT_TYPE') && WP_ENVIRONMENT_TYPE === 'local' && current_user_can('administrator')) { | |
wp_enqueue_style( | |
'xdebug-styles', | |
'/wp-content/plugins/doublee-base-plugin/assets/xdebug-styles.css', | |
[], | |
'1.0.0' | |
); | |
wp_enqueue_style('highlight-code', 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css', [], '11.8.0'); // base theme | |
wp_enqueue_script('highlight-js', 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js', [], '11.8.0'); | |
wp_enqueue_script('highlight-php', 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/php.min.js', [], '11.8.0'); | |
wp_enqueue_script('xdebug-markup', '/wp-content/plugins/doublee-base-plugin/assets/xdebug-markup.js', [], '1.0.0'); | |
} | |
} | |
add_action('wp_enqueue_scripts', 'doublee_make_xdebug_pretty'); | |
add_action('admin_enqueue_scripts', 'doublee_make_xdebug_pretty'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment