|
// ==UserScript== |
|
// @name Bitbucket Highlighter |
|
// @namespace https://github.com/lephuongbg |
|
// @version 0.11 |
|
// @description Stop-gap solution for highlighting bitbucket pull request |
|
// @author You |
|
// @match https://bitbucket.org/* |
|
// @grant GM_addStyle |
|
// @grant GM_getResourceText |
|
// @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.3.2/highlight.min.js |
|
// @resource style https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.3.2/styles/default.min.css |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict' |
|
|
|
// Support terraform |
|
hljs.registerLanguage('terraform', hljsDefineTerraform); |
|
|
|
const style = GM_getResourceText('style') |
|
GM_addStyle(style) |
|
GM_addStyle(`.hljs {display: inline-block; overflow-x: initial; padding: 0; background: none} pre.hljs.source {display: block;}`) |
|
|
|
// An observer callback to act on DOM changes |
|
// So that we can automatically detect when the spans for diff view are added |
|
const callback = function (mutationsList, observer) { |
|
// Ignore all pages that were not pull requests page |
|
// We had to @match all pages instead of only pull requests or branch page because Bitbucket is now a SPA app |
|
if (window.location.pathname.match(/\/[^/]+\/[^/]+\/pull-requests\/\d+/)) { |
|
highlightPullRequestPage(mutationsList) |
|
} else if (window.location.pathname.match(/\/[^/]+\/[^/]+\/branch\//)) { |
|
highlightBranchPage(mutationsList) |
|
} |
|
} |
|
|
|
const observer = new MutationObserver(callback) |
|
|
|
const config = { childList: true, subtree: true } |
|
const toObserveNode = document.getElementById('compare-tabs') // branch page |
|
|| document.getElementById('root') // PR page |
|
if (!toObserveNode) return |
|
observer.observe(toObserveNode, config) |
|
|
|
// Flag that marks that we are highlighting current page |
|
let highlighting = false |
|
|
|
function highlightPullRequestPage(mutationsList) { |
|
// Bail early if highlighting is already scheduled |
|
if (highlighting) return |
|
|
|
// Check if there is an observed diff article DOM node which has been reused in "load files: individually" mode |
|
let article = mutationsList.find(mutation => mutation.type === 'attributes')?.target |
|
|
|
// Also check if there is any non-highlighted article |
|
if (!article) { |
|
article = document.querySelector('article:not([highlighted-for])') |
|
// this node may be reused so we need to detect that |
|
article && observer.observe(article, { attributes: true, attributeFilter: ["aria-label"] }) |
|
} |
|
|
|
// If user clicks on show more button on top/bottom of an article, the highlight in the article will be reset |
|
if (!article) { |
|
// Detect if show more button was clicked |
|
let showMore = mutationsList.find(mutation => mutation.target.classList.contains('show-more-lines-wrapper'))?.target |
|
// Then find its article |
|
article = showMore?.closest('article') |
|
} |
|
|
|
// If there is no article to highlight then bail |
|
if (!article) { |
|
return |
|
} |
|
|
|
// Start scheduling highlighting process |
|
highlighting = true |
|
requestIdleCallback(() => { |
|
highlightArticle(article) |
|
|
|
// Mark that current iteration finishes |
|
highlighting = false |
|
|
|
// Trigger highlightPullRequestPage again to process any article left |
|
requestIdleCallback(() => highlightPullRequestPage([])) |
|
}) |
|
} |
|
|
|
function highlightArticle(article) { |
|
// Try to get the extension of the file |
|
const ext = article.getAttribute('aria-label').match(/\.(\w+)$/)?.[1] |
|
if (!ext) { |
|
return |
|
} else if (ext === 'vue') { |
|
// allowing hljs to guess the language inside .vue |
|
article.querySelectorAll('[data-qa=code-line] pre > span:last-child').forEach((node) => { |
|
hljs.highlightBlock(node) |
|
}) |
|
} else if (!hljs.getLanguage(ext)) { |
|
// quit if this is not a language supported by hljs |
|
return |
|
} else { |
|
// Create a holder to hold all codes from the same file |
|
let code = document.createElement('code') |
|
// set the language so hljs do not have to guess |
|
code.classList.add('language-' + ext) |
|
|
|
// Get all lines from the same file and put it into the holder |
|
const nodes = article.querySelectorAll('[data-qa=code-line] pre > span:last-child') |
|
code.textContent = Array.from(nodes).map(node => node.innerText).join('\n') |
|
|
|
// Then highlight the holder |
|
hljs.highlightBlock(code) |
|
|
|
// After that, split the holder to get the highlighted figments then inject them back |
|
const highlightedNodes = code.innerHTML.split('\n') |
|
nodes.forEach((_node, idx) => { |
|
_node.classList.add('language-' + ext) |
|
_node.classList.add('hljs') |
|
_node.innerHTML = highlightedNodes[idx] |
|
}) |
|
} |
|
article.setAttribute('highlighted-for', article.getAttribute('aria-label')) |
|
} |
|
|
|
function highlightBranchPage(mutationsList) { |
|
let sourceCodeNode = null |
|
|
|
// Find the source code in newly added nodes |
|
for (const mutation of mutationsList) { |
|
for (const addedNode of mutation.addedNodes) { |
|
if (!addedNode.querySelector?.('pre.source')) continue |
|
sourceCodeNode = addedNode |
|
break |
|
} |
|
if (sourceCodeNode) break |
|
} |
|
|
|
if (!sourceCodeNode) return |
|
|
|
for (const section of sourceCodeNode.querySelectorAll('section.bb-udiff')) { |
|
requestIdleCallback(() => { |
|
const ext = section.getAttribute('data-path').match(/\.(\w+)$/)?.[1] |
|
if (!ext) { |
|
return |
|
} |
|
if (ext === 'vue') { |
|
section.querySelectorAll('pre.source').forEach((node) => hljs.highlightBlock(node)) |
|
return |
|
} |
|
if (!hljs.getLanguage(ext)) { |
|
// quit if this is not a language supported by hljs |
|
return |
|
} |
|
|
|
// Create a holder to hold all codes from the same file |
|
let code = document.createElement('code') |
|
// set the language so hljs do not have to guess |
|
code.classList.add('language-' + ext) |
|
|
|
// Get all lines from the same file and put it into the holder |
|
const nodes = section.querySelectorAll('pre.source') |
|
code.textContent = Array.from(nodes).map(node => node.innerText).join('\n') |
|
|
|
// Then highlight the holder |
|
hljs.highlightBlock(code) |
|
|
|
// After that, split the holder to get the highlighted figments then inject them back |
|
const highlightedNodes = code.innerHTML.split('\n') |
|
nodes.forEach((_node, idx) => { |
|
_node.classList.add('language-' + ext) |
|
_node.classList.add('hljs') |
|
_node.innerHTML = highlightedNodes[idx] |
|
}) |
|
}) |
|
} |
|
} |
|
|
|
// highlightjs/highlightjs-terraform@73b76da/terraform.js |
|
function hljsDefineTerraform(hljs) { |
|
var NUMBERS = { |
|
className: 'number', |
|
begin: '\\b\\d+(\\.\\d+)?', |
|
relevance: 0 |
|
}; |
|
var STRINGS = { |
|
className: 'string', |
|
begin: '"', |
|
end: '"', |
|
contains: [{ |
|
className: 'variable', |
|
begin: '\\${', |
|
end: '\\}', |
|
relevance: 9, |
|
contains: [{ |
|
className: 'string', |
|
begin: '"', |
|
end: '"' |
|
}, { |
|
className: 'meta', |
|
begin: '[A-Za-z_0-9]*' + '\\(', |
|
end: '\\)', |
|
contains: [ |
|
NUMBERS, { |
|
className: 'string', |
|
begin: '"', |
|
end: '"', |
|
contains: [{ |
|
className: 'variable', |
|
begin: '\\${', |
|
end: '\\}', |
|
contains: [{ |
|
className: 'string', |
|
begin: '"', |
|
end: '"', |
|
contains: [{ |
|
className: 'variable', |
|
begin: '\\${', |
|
end: '\\}' |
|
}] |
|
}, { |
|
className: 'meta', |
|
begin: '[A-Za-z_0-9]*' + '\\(', |
|
end: '\\)' |
|
}] |
|
}] |
|
}, |
|
'self'] |
|
}] |
|
}] |
|
}; |
|
|
|
return { |
|
aliases: ['tf', 'hcl'], |
|
keywords: 'resource variable provider output locals module data terraform|10', |
|
literal: 'false true null', |
|
contains: [ |
|
hljs.COMMENT('\\#', '$'), |
|
NUMBERS, |
|
STRINGS |
|
] |
|
} |
|
} |
|
})() |
@RobertChrist Congrats on your PR, as I noticed that it has been merged recently.
Just want to inform you that I've updated my script to make it work properly on new "Load files: individually" mode (which is a godsend for large PR), since you might also want to port the change to Bitbucket Refined.
The gist is that, in "Load files: individually" mode, the
article
node can be reused completely without triggeringchildList
type mutation. Fortunately when anarticle
node is reused to show new file, thearia-label
will change. We just need to observearticle
nodes' attributes change then re-highlight them.