-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
main.ts
232 lines (200 loc) · 8.74 KB
/
main.ts
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
///<reference path="../../../.WebStorm2019.1/config/javascript/extLibs/global-types/node_modules/@types/chrome/index.d.ts"/>
/**
* This is the main script that reads the document and updates any Arabic script text
*/
// TODO good thing to add is to allow for symbols and numbers between Arabic or at the end or beginning of it
// like hashtags # and numbers and exclamation marks etc
const arabicRegEx = new RegExp('([\u0600-\u06FF\u0750-\u077F\u08a0-\u08ff\uFB50-\uFDFF\uFE70-\uFEFF]+(' +
' [\u0600-\u06FF\u0750-\u077F\u08a0-\u08ff\uFB50-\uFDFF\uFE70-\uFEFF\W\d]+)*)', 'g');
let observer: MutationObserver;
interface Array<T> {
contains(element: T): boolean;
}
/**
* Extension function for a contains function in an array
* @param element the element to check whether is in this array or not
* @return true if the element exists in this array, false otherwise
*/
Array.prototype.contains = function <T>(element: T): boolean {
let result = false;
for (let i = 0; i < this.length; i++) {
if (element === this[i]) {
result = true;
break;
}
}
return result;
};
/**
* Returns whether the given node has any Arabic script or not, this is any script that matches arabicRegEx
* @param node the node to check
* @return true if the node contains any arabic script, false otherwise
*/
function hasArabicScript(node: Node): boolean {
return !!(node.nodeValue && (node.nodeValue.trim() != "") && (node.nodeValue.match(arabicRegEx)));
}
/**
* Gets all nodes within the passed in node that have any Arabic text
* @param rootNode the node to use as the root of the traversal
* @return an array of nodes that contain all the nodes with Arabic text that are children of the passed in
* root node
*/
function getArabicTextNodesIn(rootNode: Node): Array<Node> {
let walker: TreeWalker = document.createTreeWalker(
rootNode,
NodeFilter.SHOW_ALL
);
let node: Node;
let arabicTextNodes: Array<Node> = [];
// noinspection JSAssignmentUsedAsCondition
while (node = walker.nextNode()) {
if (hasArabicScript(node)) {
arabicTextNodes.push(node);
}
}
return arabicTextNodes;
}
/**
* Sets the node's html content to the passed in html string
* @param node the node to change the html of
* @param html the html string that will be the passed in node's html
*/
function setNodeHtml(node: Node, html: string) {
let parent: Node = node.parentNode;
if (!parent || !node) return;
// don't change anything if this node or its parent are editable
if (isEditable(parent) || isEditable(node)) return;
let nextSibling = node.nextSibling;
// the div is temporary and doesn't show up in the html
let newElement = document.createElement("div");
newElement.innerHTML = html;
while (newElement.firstChild) {
// we only insert the passed in html, the div is not inserted
parent.insertBefore(newElement.firstChild, nextSibling);
}
parent.removeChild(node);
}
/**
* Checks whether the passed in node is editable or not.
* An editable node is one that returns true to isContentEditable or has a tag name as
* any one of the following:
* "textarea", "input", "text", "email", "number", "search", "tel", "url", "password"
*
* @param node the node to check
* @return true if the node is editable and false otherwise
*/
function isEditable(node: Node): boolean {
let element = node as HTMLElement;
let nodeName: string = element.nodeName.toLowerCase();
let editables = ["textarea", "input", "text", "email", "number", "search", "tel", "url", "password"];
return (element.isContentEditable || (element.nodeType === Node.ELEMENT_NODE && editables.contains(nodeName)));
}
/**
* Updates the passed in node's html to have the properties of a modified Arabic text node, this will
* replace any text that matches arabicRegEx to be a span with the font size and line height specified by
* the user's options, the span will have a class='ar', this can be used to check if the text has been
* updated by this function or not
* @param node the node to update
* @param textSize the size to update the text to
* @param lineHeight the height to update the line to
* @param font the name of the font to update the text to
*/
function updateNode(node: Node, textSize: number, lineHeight: number, font: string = "Droid Arabic Naskh") {
if (node.nodeValue) {
let newSize = textSize / 100;
let newHeight = lineHeight / 100;
let newHTML: string;
if (font === "Original") {
newHTML = "<span class='ar'' style='" +
"font-size:" + newSize + "em;" +
"line-height:" + newHeight + "em;" +
"'>$&</span>";
} else {
newHTML = "<span class='ar'' style='" +
"font-size:" + newSize + "em;" +
"line-height:" + newHeight + "em;" +
"font-family:" + "\"" + font + "\"" + "," + "sans-serif;" +
"'>$&</span>";
}
let text: string = node.nodeValue.replace(arabicRegEx, newHTML);
setNodeHtml(node, text);
}
}
/**
* Updates all Arabic script nodes in this document's body by calling updateNode() on each node in this document
* with Arabic script
* @param textSize the size to update the text to
* @param lineHeight the height to update the line to
* @param font the name of the font to update the text to
*/
function updateAll(textSize: number, lineHeight: number, font: string = "Droid Arabic Naskh") {
getArabicTextNodesIn(document.body).forEach((it: Node) => updateNode(it, textSize, lineHeight, font));
}
/**
* Starts the observer that will observe for any additions to the document and update them if they have any
* Arabic text and they have not been updated yet
* @param textSize the size to update the text to
* @param lineHeight the height to update the line to
* @param font the name of the font to update the text to
*/
function startObserver(textSize: number, lineHeight: number, font: string = "Droid Arabic Naskh") {
let config: MutationObserverInit = {
attributes: false,
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: false,
};
let callback: MutationCallback = function (mutationsList: MutationRecord[]) {
mutationsList.forEach((record: MutationRecord) => {
// If something has been added
if (record.addedNodes.length > 0) {
// For each added node
record.addedNodes.forEach((addedNode: Node) => {
// For each node with Arabic script in addedNode
getArabicTextNodesIn(addedNode).forEach((arabicNode: Node) => {
// Update arabicNode only if it hasn't been updated
if (arabicNode.parentElement && arabicNode.parentElement.getAttribute("class") != "ar") {
updateNode(arabicNode, textSize, lineHeight, font);
}
});
});
}
});
};
observer = new MutationObserver(callback);
observer.observe(document.body, config);
}
/**
* Main execution:
* Updates all existing text according to the options
* Then starts an observer with those same options to update any new text that will come
* This only happens if the on off switch is on and the site is not whitelisted
*/
chrome.storage.sync.get(["textSize", "lineHeight", "onOff", "font", "whitelisted"], (fromStorage) => {
let textSize: number = fromStorage.textSize;
let lineHeight: number = fromStorage.lineHeight;
let checked: boolean = fromStorage.onOff;
let font: string = fromStorage.font;
let whitelisted: Array<string> = fromStorage.whitelisted;
let isWhitelisted = whitelisted.contains(new URL(document.URL).hostname);
// Only do anything if the switch is on and this site is not whitelisted
if (checked && !isWhitelisted) {
updateAll(textSize, lineHeight, font);
startObserver(textSize, lineHeight, font);
}
});
/**
* Listener to update text if options are modified, the options being text size, line height and font
* Since the original font is not saved, reverting the text to it's original form is not possible
* This will disconnect the previous observer and start a new one its place with the new options
* The check whether the switch is on or if this site is whitelisted is not done here but at the
* sender's sendMessage call
*/
chrome.runtime.onMessage.addListener(function (message) {
let newSize = 100 * (message.newSize / message.oldSize);
let newHeight = 100 * (message.newHeight / message.oldHeight);
updateAll(newSize, newHeight, message.font);
observer.disconnect();
startObserver(newSize, newHeight, message.font);
});