-
Notifications
You must be signed in to change notification settings - Fork 0
/
blocker.js
152 lines (111 loc) · 4.22 KB
/
blocker.js
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Only four languages supported at this time. To add another, insert it as
* an entry into the languages object, using the language code as the key
* and the translation of 'Sponsored' (lowercase) as the value.
*/
const languages = {
"en": "sponsored",
"pt": "patrocinado",
"es": "publicidad",
"fr": "sponsorisé"
};
main();
function hasSponsoredText(element) {
let message = "";
/**
* Facebook's 'Sponsored' text consists of visble and invisible span elements.
* Due to a facebook bug, visible span elements have a class-value ending in a
* blank space. We can use this to look up those elements.
*/
for (const node of element.querySelectorAll("span[class]")) {
if (node.matches("span[class$=' ']")) {
if (node instanceof Element) {
message = message + node.textContent[0];
} else if (node instanceof Text) {
message = message + node.nodeValue;
}
}
}
return Object.values(languages).includes(message.toLowerCase());
}
function removeSponsored(node) {
/**
* Performance monitoring
*/
const start = performance.now();
/**
* Facebook's code is a little buggy, in that it leaves a trailing
* space at the end of span elements belonging to the 'Sponsored' string.
* We can use this bug to locate sponsored elements.
*/
const elementList = node.querySelectorAll("a[href='#'][role='link']");
if (elementList.length > 0) {
for (const element of elementList) {
/**
* Handing it off to hasSponsoredText to see if the element's child nodes
* form the target text. We pass the parent <a> to determine if it contains
* the 'Sponsored' text.
*/
if (hasSponsoredText(element)) {
/**
* Sponsored content in the main feed is nested within a [role='article']
* element. Similar content on the right-column is not. We look for either
* pattern in the element's path.
*/
const container = element.closest("[role='article'], div > span > div");
/**
* The final stage is to remove the Sponsored content from the page.
*/
if (container instanceof Element) {
container.remove();
console.log("Removed an item.");
}
}
}
}
const runtime = (performance.now() - start).toFixed(3);
console.info(`Handled ${elementList.length} items in ${runtime}ms`);
};
function setupObserver(root, handler) {
/**
* Call the handler once up front to catch anything
* that might have been present in the initial HTML.
*/
handler(root);
/**
* Bind a mutation observer to the root element, so that
* we can be informed of added nodes.
*/
const options = { childList: true, subtree: true };
const observer = new MutationObserver(changes => {
console.group(`Mutation Event Group: ${Date.now()}`);
const start = performance.now();
for (const change of changes) {
for (const node of change.addedNodes) {
if (node instanceof Element) {
handler(node);
}
}
}
const runtime = (performance.now() - start).toFixed(3);
console.log(`Total Time: ${runtime}ms`);
console.groupEnd();
});
observer.observe(root, options);
}
function main() {
const language = document.documentElement.lang;
/**
* If we don't support the user's language, we shouldn't
* proceed to setup any mutation observers.
*/
if (language && language in languages === false) {
console.warn(`${language} is not supported at this time.`);
return;
}
/**
* Listening for mutations from the documentElement, since it
* will persist as the user moves around Facebook.
*/
setupObserver(document.documentElement, removeSponsored);
}