Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ A browser extension that brings syntax highlighting to file diffs in Azure DevOp
- **Language Detection:** Detects the programming language based on file extensions.
- **Theme Support:** Seamlessly integrates with both light and dark themes in Azure DevOps.
- **Powered by Prism:** Utilizes the popular [Prism](https://prismjs.com/) library for fast and accurate highlighting.
- **Custom Domains Support**: Works with self-hosted (on-premise) and other custom Azure DevOps domains via a simple configuration page.

## Screenshots

Expand Down
46 changes: 46 additions & 0 deletions background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
importScripts('browser-polyfill.min.js');

function injectContent(tabId) {
console.log(`ADO Syntax Highlighter: Injecting into custom host on tab ${tabId}`);
browser.scripting.insertCSS({
target: { tabId: tabId },
files: ["prism/prism.css", "custom_styles.css"],
}).catch(err => console.warn(`CSS injection warning: ${err.message}`));

browser.scripting.executeScript({
target: { tabId: tabId },
files: ["prism/prism.js", "content_script.js"],
});
}

browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== 'complete' || !tab.url || !tab.url.startsWith('http')) {
return;
}

const manifest = browser.runtime.getManifest();
const defaultHosts = manifest.host_permissions || [];
const isDefaultHost = defaultHosts.some(pattern => {
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
return regex.test(tab.url);
});

if (isDefaultHost) {
return;
}

const { customHosts = [] } = await browser.storage.sync.get('customHosts');
if (customHosts.length === 0) {
return;
}

for (const pattern of customHosts) {
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');

if (regex.test(tab.url)) {
console.log(`URL "${tab.url}" matched custom pattern "${pattern}". Injecting scripts.`);
injectContent(tabId);
return;
}
}
});
8 changes: 8 additions & 0 deletions browser-polyfill.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@
"$schema": "https://json.schemastore.org/chrome-manifest",
"manifest_version": 3,
"name": "Syntax Highlighter for Azure DevOps",
"version": "0.5.0",
"version": "0.6.0",
"description": "Enhances code readability by adding syntax highlighting to code views and diffs within Azure DevOps, including pull requests.",
"icons": {
"16": "assets/icons/icon16.png",
"32": "assets/icons/icon32.png",
"48": "assets/icons/icon48.png",
"128": "assets/icons/icon128.png"
},
"permissions": [
"storage",
"scripting"
],
"host_permissions": [
"*://*.dev.azure.com/*",
"*://*.visualstudio.com/*"
],
"optional_host_permissions": [
"*://*/*"
],
"background": {
"service_worker": "background.js"
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"content_scripts": [
{
"matches": [
Expand Down
32 changes: 32 additions & 0 deletions options.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
</html>
<!DOCTYPE html>
<html>
<head>
<title>Syntax Highlighter for Azure DevOps Options</title>
</head>
<body>

<h1>Syntax Highlighter for Azure DevOps Options</h1>
<hr>

<h2>Granted Hosts</h2>

<h3>Default Hosts (Always Active)</h3>
<ul id="default-hosts-list">
</ul>

<h3>Manage Custom Hosts</h3>
<p>
Enter a hostname (<code>devops.mycompany.com</code>) or a specific path (<code>devops.mycompany.com/ado</code>). The extension will activate on that page and any sub-pages.
</p>

<input type="text" id="host-input" placeholder="e.g., devops.mycompany.com" size="40">
<button id="add-host-btn">Add Host</button>
<ul id="hosts-list">
</ul>

<script src="browser-polyfill.min.js"></script>
<script src="options.js"></script>

</body>
</html>
97 changes: 97 additions & 0 deletions options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const hostInput = document.getElementById('host-input');
const addHostBtn = document.getElementById('add-host-btn');
const hostsList = document.getElementById('hosts-list');
const defaultHostsList = document.getElementById('default-hosts-list');

function createCustomHostListItem(host) {
const listItem = document.createElement('li');
listItem.textContent = host;

const removeBtn = document.createElement('button');
removeBtn.textContent = 'Remove';
removeBtn.style.marginLeft = '10px';
removeBtn.addEventListener('click', () => removeHost(host));

listItem.appendChild(removeBtn);
hostsList.appendChild(listItem);
}

function loadDefaultHosts() {
const manifest = browser.runtime.getManifest();
const defaultHosts = manifest.host_permissions || [];

defaultHostsList.innerHTML = '';
defaultHosts.forEach(host => {
const listItem = document.createElement('li');
listItem.textContent = host;
defaultHostsList.appendChild(listItem);
});
}

async function loadCustomHosts() {
const { customHosts = [] } = await browser.storage.sync.get('customHosts');
hostsList.innerHTML = '';
customHosts.forEach(createCustomHostListItem);
}

async function addHost() {
let hostValue = hostInput.value.trim();
if (!hostValue) return;

hostValue = hostValue.replace(/\/+$/, '');

const permissionPattern = `*://${hostValue}/*`;

try {
const granted = await browser.permissions.request({
origins: [permissionPattern]
});

if (granted) {
const { customHosts = [] } = await browser.storage.sync.get('customHosts');
if (!customHosts.includes(permissionPattern)) {
const updatedHosts = [...customHosts, permissionPattern];
await browser.storage.sync.set({ customHosts: updatedHosts });
createCustomHostListItem(permissionPattern);
}
hostInput.value = '';
} else {
console.warn('Permission was not granted by the user.');
}
} catch (err) {
console.error(`Error requesting permission: ${err}`);
alert(`Could not request permission for "${hostValue}". Please ensure it's a valid hostname.`);
}
}

async function removeHost(hostToRemove) {
try {
const removed = await browser.permissions.remove({
origins: [hostToRemove]
});

if (removed) {
const { customHosts = [] } = await browser.storage.sync.get('customHosts');
const updatedHosts = customHosts.filter(h => h !== hostToRemove);
await browser.storage.sync.set({ customHosts: updatedHosts });
loadCustomHosts();
} else {
console.warn('Could not remove permission.');
}
} catch (err) {
console.error(`Error removing permission: ${err}`);
}
}

addHostBtn.addEventListener('click', addHost);

hostInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addHost();
}
});

document.addEventListener('DOMContentLoaded', () => {
loadDefaultHosts();
loadCustomHosts();
});