diff --git a/assets/screenshots/small-promo-tile-440-280.png b/assets/screenshots/small-promo-tile-440-280.png new file mode 100644 index 0000000..f9c544f Binary files /dev/null and b/assets/screenshots/small-promo-tile-440-280.png differ diff --git a/content_script.js b/content_script.js index b66ff6a..6c198a0 100644 --- a/content_script.js +++ b/content_script.js @@ -1,100 +1,121 @@ // Helper to map file extensions to Prism language aliases https://prismjs.com/#supported-languages const filePatternToLanguage = { - '*.csproj': 'markup', - 'directory.build.props': 'markup', - 'directory.build.targets': 'markup', - 'directory.packages.props': 'markup', - 'nuget.config': 'markup', + '*.csproj': 'markup', + 'directory.build.props': 'markup', + 'directory.build.targets': 'markup', + 'directory.packages.props': 'markup', + 'nuget.config': 'markup', + '*.feature': 'gherkin' }; function getLanguageFromFileName(fileName) { - if (!fileName) return null; - const lowerFileName = fileName.toLowerCase(); - - for (const [pattern, value] of Object.entries(filePatternToLanguage)) { - const regexPattern = pattern - .replace(/\./g, '\\.') - .replace(/\*/g, '.*'); - const regex = new RegExp(`^${regexPattern}$`); - - if (regex.test(lowerFileName)) { - return value; - } + if (!fileName) return null; + const lowerFileName = fileName.toLowerCase(); + + for (const [pattern, value] of Object.entries(filePatternToLanguage)) { + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*'); + const regex = new RegExp(`^${regexPattern}$`); + + if (regex.test(lowerFileName)) { + return value; } - - const extension = lowerFileName.substring(lowerFileName.lastIndexOf('.') + 1); - return extension || null; + } + + const extension = lowerFileName.substring(lowerFileName.lastIndexOf('.') + 1); + return extension || null; } let theme = ""; function getTheme(element) { - if (theme) return theme; - const color = window.getComputedStyle(element).color; - - // Extract RGB components - const rgb = color.match(/\d+/g).map(Number); - let [r, g, b] = rgb; - - // Convert to relative luminance (sRGB) - [r, g, b] = [r, g, b].map((c) => { - c /= 255; - return c <= 0.03928 - ? c / 12.92 - : Math.pow((c + 0.055) / 1.055, 2.4); - }); - - const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; - - theme = luminance > 0.5 ? 'prismjs-tomorrow-night' : 'prism-one-light'; - return theme; + if (theme) return theme; + const color = window.getComputedStyle(element).color; + + // Extract RGB components + const rgb = color.match(/\d+/g).map(Number); + let [r, g, b] = rgb; + + // Convert to relative luminance (sRGB) + [r, g, b] = [r, g, b].map((c) => { + c /= 255; + return c <= 0.03928 + ? c / 12.92 + : Math.pow((c + 0.055) / 1.055, 2.4); + }); + + const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + + theme = luminance > 0.5 ? 'prismjs-tomorrow-night' : 'prism-one-light'; + return theme; } function processFileDiff(fileDiffElement) { - let fileNameElement = fileDiffElement.querySelector('.repos-change-summary-file-icon-container + .flex-column .text-ellipsis'); - - const fileName = fileNameElement ? fileNameElement.textContent.trim() : null; - const language = getLanguageFromFileName(fileName); - - let originalLineElements = fileDiffElement.querySelectorAll('.monospaced-text > .repos-line-content'); - - originalLineElements.forEach(originalLineElement => { - if (!originalLineElement.classList.contains('ado-syntax-highlighted')) { - const highlightedLine = originalLineElement.cloneNode(true); - - const code = document.createElement('code'); // Temporary element - code.className = `language-${language}`; - code.innerHTML = highlightedLine.innerHTML; - Prism.highlightElement(code, false, () => { - const contentDiv = document.createElement('div'); - contentDiv.innerHTML = code.innerHTML; - contentDiv.classList.add(getTheme(originalLineElement)); - highlightedLine.innerHTML = ''; - highlightedLine.appendChild(contentDiv); - highlightedLine.classList.add('ado-syntax-highlighted'); - - // Hide the original line. - // This is a hack to make the line comment button functional. Otherwise it breaks. - originalLineElement.style.display = 'none'; - - // Insert the highlighted version after the original - originalLineElement.parentNode.insertBefore(highlightedLine, originalLineElement.nextSibling) - }); - } - }); + if (fileDiffElement.querySelector('.ado-syntax-highlighted')) { + return; + } + + let fileNameElement = fileDiffElement.querySelector('.repos-change-summary-file-icon-container + .flex-column .text-ellipsis'); + + const fileName = fileNameElement ? fileNameElement.textContent.trim() : null; + const language = getLanguageFromFileName(fileName); + + let originalLineElements = fileDiffElement.querySelectorAll('.monospaced-text > .repos-line-content'); + + originalLineElements.forEach(originalLineElement => { + if (!originalLineElement.classList.contains('ado-syntax-highlighted')) { + + const elementsToPreserve = []; + const nonCodeQuery = '.screen-reader-only, span[aria-hidden="true"]'; + originalLineElement.querySelectorAll(nonCodeQuery).forEach(el => { + elementsToPreserve.push(el.cloneNode(true)); + }); + + const codeContainer = originalLineElement.cloneNode(true); + codeContainer.querySelectorAll(nonCodeQuery).forEach(el => el.remove()); + const codeToHighlight = codeContainer.innerHTML; + + const highlightedLine = originalLineElement.cloneNode(true); + + const code = document.createElement('code'); // Temporary element + code.className = `language-${language}`; + code.innerHTML = codeToHighlight; + Prism.highlightElement(code, false, () => { + const contentDiv = document.createElement('div'); + contentDiv.innerHTML = code.innerHTML; + contentDiv.classList.add(getTheme(originalLineElement)); + highlightedLine.innerHTML = ''; + + elementsToPreserve.forEach(el => { + highlightedLine.appendChild(el); + }); + + highlightedLine.appendChild(contentDiv); + highlightedLine.classList.add('ado-syntax-highlighted'); + + // Hide the original line. + // This is a hack to make the line comment button functional. Otherwise it breaks. + originalLineElement.style.display = 'none'; + + // Insert the highlighted version after the original + originalLineElement.parentNode.insertBefore(highlightedLine, originalLineElement.nextSibling) + }); + } + }); } function applySyntaxHighlighting() { - // Only apply highlighting if we're on a PR page - if (!window.location.href.includes('/_git/') || !window.location.href.includes('/pullrequest/')) { - return; - } + // Only apply highlighting if we're on a PR page + if (!window.location.href.includes('/_git/') || !window.location.href.includes('/pullrequest/')) { + return; + } - console.log("ADO Syntax Highlighter: Applying..."); + console.log("ADO Syntax Highlighter: Applying..."); - const fileDiffPanels = document.querySelectorAll('.repos-summary-header'); - fileDiffPanels.forEach(fileDiffPanel => { - processFileDiff(fileDiffPanel); - }); + const fileDiffPanels = document.querySelectorAll('.repos-summary-header'); + fileDiffPanels.forEach(fileDiffPanel => { + processFileDiff(fileDiffPanel); + }); } console.log("ADO Syntax Highlighter: Content script loaded."); @@ -103,41 +124,36 @@ console.log("ADO Syntax Highlighter: Content script loaded."); applySyntaxHighlighting(); function debounce(func, wait) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; } -const debouncedApplyHighlighting = debounce(applySyntaxHighlighting, 500); +const debouncedApplyHighlighting = debounce(applySyntaxHighlighting, 250); // Listen for URL changes window.addEventListener('popstate', debouncedApplyHighlighting); // Observe DOM changes for dynamically loaded content new MutationObserver((mutationsList) => { - for (const mutation of mutationsList) { - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { - let needsHighlighting = false; - mutation.addedNodes.forEach(node => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.matches?.('.repos-summary-code-diff, .vc-diff-viewer, .diff-frame, .repos-diff-contents-row, .bolt-card, .repos-pr-iteration-file-header')) { - needsHighlighting = true; - } - if (node.querySelector?.('.repos-summary-code-diff, .vc-diff-viewer, .diff-frame, .repos-diff-contents-row, .bolt-card, .repos-pr-iteration-file-header')) { - needsHighlighting = true; - } - } - }); - - if (needsHighlighting) { - debouncedApplyHighlighting(); - break; - } + for (const mutation of mutationsList) { + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + node.matches?.('.repos-summary-code-diff, .vc-diff-viewer, .diff-frame, .repos-diff-contents-row, .bolt-card, .repos-pr-iteration-file-header') || + node.querySelector?.('.repos-summary-code-diff, .vc-diff-viewer, .diff-frame, .repos-diff-contents-row, .bolt-card, .repos-pr-iteration-file-header') + ) { + debouncedApplyHighlighting(); + return; + } } + } } + } }).observe(document.body, { childList: true, subtree: true }); diff --git a/manifest.json b/manifest.json index b34f38a..9c91693 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/chrome-manifest", "manifest_version": 3, "name": "Azure DevOps PR Syntax Highlighter", - "version": "0.1.0", + "version": "0.1.1", "description": "Adds syntax highlighting to partial file diffs in Azure DevOps Pull Requests.", "icons": { "16": "assets/icons/icon16.png",