-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
103 lines (92 loc) · 3.37 KB
/
content.js
File metadata and controls
103 lines (92 loc) · 3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// Config: set to true if you want only off-site links to open new tabs
const ONLY_EXTERNAL = true;
// Helper: is a "normal" anchor we should modify?
function isGoodAnchor(a) {
if (!a || a.tagName !== 'A') return false;
const href = a.getAttribute('href') || '';
// Ignore empty, hash, javascript:, downloads, and anchors with explicit target already
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return false;
if (a.hasAttribute('download')) return false;
// If ONLY_EXTERNAL, only modify anchors that go off github.com/gist.github.com
if (ONLY_EXTERNAL) {
try {
const url = new URL(href, location.href);
const sameHost = url.host === location.host;
if (sameHost) return false;
} catch (_) {
// If URL parsing fails, skip
return false;
}
}
return true;
}
function patchAnchor(a) {
if (!isGoodAnchor(a)) return;
a.setAttribute('target', '_blank');
// security: prevent window.opener hijacking
const existingRel = (a.getAttribute('rel') || '').split(/\s+/);
const needed = new Set(['noopener', 'noreferrer']);
const merged = Array.from(new Set([...existingRel, ...needed])).filter(Boolean).join(' ');
a.setAttribute('rel', merged);
}
// Initial sweep
function sweep(root = document) {
root.querySelectorAll('a[href]').forEach(patchAnchor);
}
// Observe DOM changes (GitHub updates pages dynamically)
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.tagName === 'A') {
patchAnchor(node);
} else {
// Check descendants
node.querySelectorAll?.('a[href]').forEach(patchAnchor);
}
}
// Attributes changed
if (m.type === 'attributes' && m.target?.tagName === 'A' && m.attributeName === 'href') {
patchAnchor(m.target);
}
}
});
// Start as early as possible
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href']
});
// Just in case the page booted before our observer
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => sweep());
} else {
sweep();
}
// Fallback: capture normal left-clicks before GitHub’s PJAX/turbo handlers
// This ensures new-tab even if GitHub intercepts clicks.
document.addEventListener('click', (e) => {
// Ignore modified clicks or non-left clicks
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
// Ignore clicks from editable areas
const path = e.composedPath?.() || [];
if (path.some(n => n && n.isContentEditable)) return;
// Find nearest anchor
let el = e.target;
while (el && el !== document && el.tagName !== 'A') el = el.parentElement;
if (!el || el.tagName !== 'A') return;
if (!isGoodAnchor(el)) return;
// If the observer already set target=_blank, default behavior is fine
if (el.getAttribute('target') === '_blank') return;
// Otherwise, open manually and stop GitHub’s in-page nav
const href = el.getAttribute('href');
try {
const url = new URL(href, location.href);
window.open(url.href, '_blank', 'noopener,noreferrer');
e.preventDefault();
e.stopPropagation();
} catch (_) {
// If URL parsing fails, let it behave normally
}
}, { capture: true, passive: false });