├── README.md ├── active.png ├── background.js ├── content.js ├── inactive.png ├── manifest.json ├── popup.css ├── popup.html └── popup.js /README.md: -------------------------------------------------------------------------------- 1 | # HyperBold 2 | An accessibility extension for people with ADHD 3 | -------------------------------------------------------------------------------- /active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geeknerd1337/HyperBold/431a193960ae6f778cb8a7407c08869288080aa2/active.png -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // Define default settings 2 | const DEFAULT_SETTINGS = { 3 | enabled: true, 4 | enableHighlighting: false, 5 | highlightColor: "yellow", 6 | websites: [], 7 | blacklist: [], 8 | }; 9 | 10 | // Load settings from storage 11 | function loadSettings() { 12 | return chrome.storage.local.get(DEFAULT_SETTINGS); 13 | } 14 | 15 | // Save settings to storage 16 | function saveSettings(settings) { 17 | return chrome.storage.local.set(settings); 18 | } 19 | 20 | // When the extension is installed or updated, set default settings 21 | chrome.runtime.onInstalled.addListener((details) => { 22 | if (details.reason === "install") { 23 | saveSettings(DEFAULT_SETTINGS); 24 | } 25 | }); 26 | 27 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 28 | if (changeInfo.status == "complete" && tab.active) { 29 | loadSettings().then((settings) => { 30 | if (!tab.url) { 31 | return; 32 | } 33 | const url = new URL(tab.url); 34 | const includesWebsite = settings.websites.includes(url.href); 35 | const hostname = url.hostname; 36 | const pageBlacklist = settings.blacklist.includes(hostname); 37 | const enabled = settings.enabled; 38 | 39 | if (enabled && includesWebsite && !pageBlacklist) { 40 | chrome.action.setIcon({ path: "/active.png" }); 41 | } else { 42 | chrome.action.setIcon({ path: "/inactive.png" }); 43 | } 44 | }); 45 | } 46 | }); 47 | 48 | chrome.tabs.onActivated.addListener(function (activeInfo) { 49 | chrome.tabs.get(activeInfo.tabId, function (tab) { 50 | if (tab.active) { 51 | loadSettings().then((settings) => { 52 | //Ensure the url is valid 53 | if (!tab.url) { 54 | return; 55 | } 56 | const url = new URL(tab.url); 57 | const includesWebsite = settings.websites.includes(url.href); 58 | const hostname = url.hostname; 59 | const pageBlacklist = settings.blacklist.includes(hostname); 60 | const enabled = settings.enabled; 61 | 62 | if (enabled && includesWebsite && !pageBlacklist) { 63 | chrome.action.setIcon({ path: "/active.png" }); 64 | } else { 65 | chrome.action.setIcon({ path: "/inactive.png" }); 66 | } 67 | }); 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_SETTINGS = { 2 | enabled: true, 3 | enableHighlighting: false, 4 | highlightColor: "yellow", 5 | websites: [], 6 | blacklist: [], 7 | }; 8 | 9 | let settings; 10 | 11 | function loadSettings() { 12 | return new Promise((resolve, reject) => { 13 | chrome.storage.local.get(DEFAULT_SETTINGS, function (settings) { 14 | resolve(settings); 15 | }); 16 | }); 17 | } 18 | 19 | function saveSettings(settings) { 20 | return chrome.storage.local.set(settings); 21 | } 22 | 23 | async function runExtension() { 24 | try { 25 | settings = await loadSettings(); 26 | 27 | function ableToHighlight() { 28 | // Extract the hostname from the URL of the tab 29 | var url = window.location.href; 30 | var hostname = new URL(url).hostname; 31 | //Log if settings is undefined 32 | let isSiteEnabled = settings.websites.includes(hostname); 33 | let isPageEnabled = settings.websites.includes(url); 34 | 35 | if (!settings.websites.includes(hostname) && !settings.blacklist.includes(hostname)) { 36 | isSiteEnabled = true; 37 | settings.websites.push(hostname); 38 | saveSettings(settings); 39 | } 40 | if (!settings.websites.includes(url) && !settings.blacklist.includes(url)) { 41 | settings.websites.push(url); 42 | isPageEnabled = true; 43 | saveSettings(settings); 44 | } 45 | 46 | return isSiteEnabled && isPageEnabled; 47 | } 48 | 49 | function boldFirstHalfOfWords(element) { 50 | const highlightTag = false ? "null" : "null"; 51 | 52 | let ret = ""; 53 | // Loop through all child nodes of the element 54 | for (let i = 0; i < element.childNodes.length; i++) { 55 | const childNode = element.childNodes[i]; 56 | // If the node is a text node, process its content 57 | if (childNode.nodeType === Node.TEXT_NODE) { 58 | //If the element is just a space, skip it 59 | if (childNode.textContent === " ") { 60 | continue; 61 | } 62 | //If it's an empty string, skip it 63 | if (childNode.textContent === "") { 64 | continue; 65 | } 66 | 67 | if (childNode === undefined) { 68 | continue; 69 | } 70 | 71 | //If the text content is undefined, skip it 72 | if (childNode.textContent === undefined) { 73 | continue; 74 | } 75 | //Print the node 76 | 77 | const words = childNode.textContent.split(/(\s+)/); 78 | // Loop through all the words in the text node 79 | for (let j = 0; j < words.length; j++) { 80 | const word = words[j]; 81 | // Check if the word is a link, image, svg or iframe element 82 | if ( 83 | word.includes("${firstHalf}${secondHalf}`; 149 | 150 | //If the length of the word is 3 then just bold the first letter 151 | if (word.length === 3) { 152 | ret = `<${highlightTag} data-modified="true" ${ 153 | settings.enableHighlighting 154 | ? `style="background-color:${color};color:${text};"` 155 | : `style="font-weight:700;"` 156 | }>${word[0]}${word.slice(1)}`; 157 | } 158 | 159 | // Replace the original word with the bolded word in the text content 160 | words[j] = ret; 161 | } 162 | // Join the modified words back into a single string 163 | const newTextNode = document.createElement(null); 164 | newTextNode.innerHTML = words.join(""); 165 | 166 | newTextNode.setAttribute("data-modified", "true"); 167 | element.setAttribute("data-modified", "true"); 168 | 169 | // Replace the original text node with the new element 170 | element.replaceChild(newTextNode, childNode); 171 | } else if (childNode.nodeType === Node.ELEMENT_NODE) { 172 | // If the node is an element node, recursively process its children 173 | boldFirstHalfOfWords(childNode); 174 | } 175 | } 176 | } 177 | 178 | function highlightFirstSyllable() { 179 | // Select all

elements on the page 180 | const paragraphs = document.getElementsByTagName("p"); 181 | //List elements 182 | const lists = document.getElementsByTagName("li"); 183 | //Spans 184 | const spans = document.getElementsByTagName("span"); 185 | 186 | if (!ableToHighlight()) { 187 | return; 188 | } 189 | 190 | if (!settings.enabled) { 191 | return; 192 | } 193 | 194 | //Add elements to alltext 195 | const allText = [...paragraphs, ...lists, ...spans] 196 | .filter((element) => { 197 | return element.offsetParent !== null; 198 | }) 199 | .filter((element) => { 200 | //Remove all spans that have as its only child 201 | if (element.tagName === "SPAN") { 202 | //If the current site is reddit, then don't do anything 203 | //THis is so hacky but reddit will not function with span tags for some reason 204 | const isReddit = currentURL.includes("reddit.com"); 205 | if (isReddit) { 206 | return false; 207 | } 208 | } 209 | return true; 210 | }); 211 | 212 | const s = settings; 213 | 214 | //Set the checkbox to the correct value 215 | 216 | settings.enabled = s.enabled; 217 | settings.enableHighlighting = s.enableHighlighting; 218 | settings.highlightColor = s.highlightColor; 219 | 220 | if (!settings.enabled) { 221 | return; 222 | } 223 | 224 | // Iterate over each

element 225 | for (const text of allText) { 226 | boldFirstHalfOfWords(text); 227 | } 228 | } 229 | 230 | const currentURL = window.location.href; 231 | 232 | // Select the node that will be observed for changes 233 | const targetNode = document.body; 234 | 235 | // Create an observer instance 236 | const observer = new MutationObserver((mutationsList) => { 237 | if (!ableToHighlight()) { 238 | return; 239 | } 240 | 241 | if (!settings.enabled) { 242 | return; 243 | } 244 | const currentURL = window.location.href; 245 | let processedNodes = new Set(); 246 | 247 | //Get all the elements from the mutation list that don't have the 'data-mofified' attribute 248 | const elements = mutationsList 249 | .filter((mutation) => { 250 | return mutation.type === "childList"; 251 | }) 252 | .map((mutation) => { 253 | //Get any paragraph elements in the target 254 | const paragraphs = mutation.target.getElementsByTagName("p"); 255 | const lists = mutation.target.getElementsByTagName("li"); 256 | 257 | const spans = mutation.target.getElementsByTagName("span"); 258 | 259 | const allText = [...paragraphs, ...lists, ...spans] 260 | .filter((element) => { 261 | return element.offsetParent !== null; 262 | }) 263 | .filter((element) => { 264 | //Filter elements that have no innter text 265 | return element.innerText.length > 0; 266 | }); 267 | 268 | return allText.flat(); 269 | }) 270 | .flat() 271 | .filter((element) => { 272 | //Remove all spans that have as its only child 273 | if (element.tagName === "SPAN") { 274 | //If the current site is reddit, then don't do anything 275 | //THis is so hacky but reddit will not function with span tags for some reason 276 | const isReddit = currentURL.includes("reddit.com"); 277 | if (isReddit) { 278 | return false; 279 | } 280 | } 281 | return true; 282 | }) 283 | .filter((element) => { 284 | if (element !== undefined) { 285 | const isModified = element.getAttribute("data-modified") === "true"; 286 | if (!isModified && !processedNodes.has(element)) { 287 | processedNodes.add(element); 288 | element.setAttribute("data-modified", "true"); 289 | return true; 290 | } 291 | return false; 292 | } 293 | }); 294 | 295 | //If there are no elements, then return 296 | if (elements.length === 0) { 297 | return; 298 | } 299 | 300 | // Iterate over each

element 301 | for (const element of elements) { 302 | boldFirstHalfOfWords(element); 303 | } 304 | }); 305 | 306 | // Start observing the target node for configured mutations 307 | observer.observe(targetNode, { childList: true, subtree: true }); 308 | 309 | document.body.addEventListener("load", () => { 310 | highlightFirstSyllable(); 311 | }); 312 | 313 | document.addEventListener("DOMContentLoaded", highlightFirstSyllable()); 314 | } catch (e) { 315 | console.log(e); 316 | } 317 | } 318 | 319 | //Call run extensions 320 | runExtension(); 321 | -------------------------------------------------------------------------------- /inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Geeknerd1337/HyperBold/431a193960ae6f778cb8a7407c08869288080aa2/inactive.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hyper Bold", 3 | "version": "0.0.0.1", 4 | "manifest_version": 3, 5 | "description": "An accessability extension for those with ADHD who struggle to read text on the web.", 6 | "permissions": [ 7 | "activeTab", 8 | "storage", 9 | "tabs" 10 | ], 11 | "background": { 12 | "service_worker": "background.js" 13 | }, 14 | "action": { 15 | 16 | "default_title": "Highlight First Syllable", 17 | "default_popup": "popup.html", 18 | "default_icon": "active.png" 19 | }, 20 | "icons":{ 21 | "128": "active.png" 22 | }, 23 | "content_scripts": [ 24 | { 25 | "matches": [""], 26 | "js": ["content.js"], 27 | "run_at": "document_idle" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: rgb(29, 31, 33); 3 | background: linear-gradient(128deg, rgba(29, 31, 33, 1) 0%, rgba(112, 120, 128, 1) 100%); 4 | width: fit-content; 5 | } 6 | 7 | body { 8 | background-color: #1d1f21; 9 | padding: 10px; 10 | width: fit-content; 11 | } 12 | 13 | .submit { 14 | background-color: #373b41; 15 | border: 1px solid #1d1f21; 16 | color: white; 17 | font-size: 16px; 18 | padding: 10px; 19 | border-radius: 5px; 20 | cursor: pointer; 21 | margin-bottom: 10px; 22 | transition: all 0.2s ease-in-out; 23 | } 24 | 25 | .submit:hover { 26 | transform: scale(1.05); 27 | } 28 | 29 | .centerElement { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-direction: column; 34 | 35 | width: 100%; 36 | } 37 | 38 | .title { 39 | text-align: center; 40 | color: white; 41 | font-size: 24px; 42 | } 43 | 44 | .formHolder { 45 | width: 100%; 46 | height: 100%; 47 | align-items: center; 48 | justify-content: center; 49 | display: flex; 50 | flex-direction: column; 51 | } 52 | 53 | .formLabel { 54 | color: white; 55 | font-size: 16px; 56 | } 57 | 58 | .siteHeader { 59 | color: white; 60 | font-size: 16px; 61 | margin-bottom: 0px; 62 | } 63 | 64 | a{ 65 | color: #2196f3; 66 | text-decoration: underline; 67 | 68 | } 69 | 70 | a:hover{ 71 | color: #2196f3; 72 | } 73 | 74 | a:active{ 75 | color: #2196f3; 76 | } 77 | 78 | a:visited{ 79 | color: lightpink; 80 | } 81 | 82 | .siteLabel { 83 | color: lightgray; 84 | display: flex; 85 | text-overflow: clip; 86 | overflow: hidden; 87 | white-space: nowrap; 88 | 89 | 90 | min-width: 150px; 91 | } 92 | 93 | .verticalGroup { 94 | 95 | } 96 | 97 | .disabled{ 98 | opacity: 0.5; 99 | pointer-events: none; 100 | transform: scale(0.8); 101 | } 102 | 103 | .switchHolder { 104 | display: flex; 105 | flex-direction: row; 106 | width: 100%; 107 | justify-content: space-between; 108 | align-items: center; 109 | gap: 10px; 110 | } 111 | 112 | /* The switch - the box around the slider */ 113 | .switch { 114 | position: relative; 115 | display: flex; 116 | justify-content: center; 117 | align-items: center; 118 | width: 50px; 119 | height: 24px; 120 | } 121 | 122 | /* Hide default HTML checkbox */ 123 | .switch input { 124 | opacity: 0; 125 | width: 0; 126 | height: 0; 127 | } 128 | 129 | /* The slider */ 130 | .slider { 131 | position: absolute; 132 | cursor: pointer; 133 | 134 | top: 0; 135 | left: 0; 136 | right: 0; 137 | bottom: 0; 138 | background-color: #ccc; 139 | -webkit-transition: 0.4s; 140 | transition: 0.4s; 141 | } 142 | 143 | .slider:before { 144 | position: absolute; 145 | content: ""; 146 | height: 20px; 147 | width: 20px; 148 | left: 2px; 149 | bottom: 2px; 150 | 151 | aspect-ratio: 1/1; 152 | 153 | background-color: white; 154 | -webkit-transition: 0.4s; 155 | transition: 0.4s; 156 | } 157 | 158 | input:checked + .slider { 159 | background-color: #2196f3; 160 | } 161 | 162 | input:focus + .slider { 163 | box-shadow: 0 0 1px #2196f3; 164 | } 165 | 166 | input:checked + .slider:before { 167 | -webkit-transform: translateX(26px); 168 | -ms-transform: translateX(26px); 169 | transform: translateX(26px); 170 | } 171 | 172 | /* Rounded sliders */ 173 | .slider.round { 174 | border-radius: 34px; 175 | } 176 | 177 | .slider.round:before { 178 | border-radius: 50%; 179 | } 180 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Highlight First Syllable Settings 8 | 9 | 10 | 11 | 12 |

13 |

HyperBold

14 |
15 |
16 |
17 |
18 |

Enabled

19 | 24 |
25 |
26 |

Use Hilight

27 | 32 |
33 | 34 |
35 |

Hilight Color

36 | 44 |
45 |
46 |
47 | 48 |
49 |

This Page:

50 |
51 |
52 | 56 |
57 | 58 |
59 |
60 | 61 |
62 |

This Page:

63 |
64 |
65 | 69 |
70 | 71 |
72 | 73 |
74 |
75 | 80 |
81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_SETTINGS = { 2 | enabled: true, 3 | enableHighlighting: false, 4 | highlightColor: "yellow", 5 | websites: [], 6 | blacklist: [], 7 | }; 8 | 9 | let mySettings = DEFAULT_SETTINGS; 10 | 11 | window.onload = function () { 12 | loadSettings(); 13 | 14 | const form = document.querySelector("#settings-form"); 15 | const sitesControl = document.getElementById("site-switch"); 16 | 17 | const pageControl = document.getElementById("page-switch"); 18 | const enabledControl = document.getElementById("enabled"); 19 | const submitButton = document.getElementById("submitButton"); 20 | 21 | function toggleSubmitActive() { 22 | submitButton.classList.remove("disabled"); 23 | } 24 | 25 | const enableHighlightingCheckbox = document.getElementById( 26 | "enable-highlighting" 27 | ); 28 | const highlightColorSelect = document.getElementById("highlight-color"); 29 | 30 | //When the site switched is changed, if it's true, add the current site to the list of websites, if it's false, remove it 31 | sitesControl.addEventListener("change", (event) => { 32 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 33 | // Extract the hostname from the URL of the tab 34 | var url = tabs[0].url; 35 | var hostname = new URL(url).hostname; 36 | 37 | if (event.target.checked) { 38 | if (!mySettings.websites.includes(hostname)) { 39 | mySettings.websites.push(hostname); 40 | } 41 | //remove it from the blacklist if it's there 42 | mySettings.blacklist = mySettings.blacklist.filter( 43 | (site) => site !== hostname 44 | ); 45 | 46 | if (mySettings.websites.includes(url)) { 47 | pageControl.checked = true; 48 | } 49 | } else { 50 | mySettings.websites = mySettings.websites.filter( 51 | (site) => site !== hostname 52 | ); 53 | //add it to the blacklist if it's not there 54 | if (!mySettings.blacklist.includes(hostname)) { 55 | mySettings.blacklist.push(hostname); 56 | } 57 | pageControl.checked = false; 58 | } 59 | toggleSubmitActive(); 60 | saveSettings(mySettings); 61 | }); 62 | }); 63 | 64 | //When the page switched is changed, if it's true, enable the extension for the current page, if it's false, disable it 65 | pageControl.addEventListener("change", (event) => { 66 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 67 | // Extract the hostname from the URL of the tab 68 | var url = tabs[0].url; 69 | 70 | if (event.target.checked) { 71 | if (!mySettings.websites.includes(url)) { 72 | mySettings.websites.push(url); 73 | } 74 | //remove it from the blacklist if it's there 75 | mySettings.blacklist = mySettings.blacklist.filter( 76 | (site) => site !== url 77 | ); 78 | } else { 79 | mySettings.websites = mySettings.websites.filter( 80 | (site) => site !== url 81 | ); 82 | //add it to the blacklist if it's not there 83 | if (!mySettings.blacklist.includes(url)) { 84 | mySettings.blacklist.push(url); 85 | } 86 | } 87 | toggleSubmitActive(); 88 | saveSettings(mySettings); 89 | }); 90 | }); 91 | 92 | //When the enabled checkbox is changed, if it's true, enable the extension, if it's false, disable it 93 | enabledControl.addEventListener("change", (event) => { 94 | if (event.target.checked) { 95 | mySettings.enabled = true; 96 | } else { 97 | mySettings.enabled = false; 98 | } 99 | toggleSubmitActive(); 100 | saveSettings(mySettings); 101 | }); 102 | 103 | //When the enable highlighting checkbox is changed, if it's true, enable highlighting, if it's false, disable it 104 | enableHighlightingCheckbox.addEventListener("change", (event) => { 105 | if (event.target.checked) { 106 | mySettings.enableHighlighting = true; 107 | if ( 108 | enabledControl.checked && 109 | sitesControl.checked && 110 | pageControl.checked 111 | ) { 112 | submitButton.classList.remove("disabled"); 113 | } 114 | } else { 115 | mySettings.enableHighlighting = false; 116 | } 117 | 118 | saveSettings(mySettings); 119 | }); 120 | 121 | highlightColorSelect.addEventListener("change", (event) => { 122 | mySettings.highlightColor = event.target.value; 123 | toggleSubmitActive(); 124 | saveSettings(mySettings); 125 | }); 126 | 127 | //When the settings form is submitted, save the settings 128 | form.addEventListener("submit", (event) => { 129 | event.preventDefault(); 130 | //Refresh the page 131 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 132 | chrome.tabs.reload(tabs[0].id); 133 | }); 134 | }); 135 | }; 136 | 137 | //Handle the loading of settings from storage 138 | function loadSettings() { 139 | return chrome.storage.local.get(DEFAULT_SETTINGS, function (settings) { 140 | //Set the checkbox to the correct value 141 | document.getElementById("enabled").checked = settings.enabled; 142 | document.getElementById("enable-highlighting").checked = 143 | settings.enableHighlighting; 144 | document.getElementById("highlight-color").value = settings.highlightColor; 145 | mySettings = settings; 146 | submitButton.classList.add("disabled"); 147 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 148 | // Extract the hostname from the URL of the tab 149 | var url = tabs[0].url; 150 | var hostname = new URL(url).hostname; 151 | 152 | let isSiteEnabled = mySettings.websites.includes(hostname); 153 | let isPageEnabled = 154 | mySettings.websites.includes(url) && 155 | !mySettings.blacklist.includes(hostname); 156 | 157 | //If the site doesn't exist in mySettings.websites and it doesn't exist in the blacklist, add it to the websites 158 | if ( 159 | !isSiteEnabled && 160 | !mySettings.blacklist.includes(hostname) && 161 | !mySettings.websites.includes(hostname) 162 | ) { 163 | if (!mySettings.websites.includes(hostname)) { 164 | mySettings.websites.push(hostname); 165 | } 166 | isSiteEnabled = true; 167 | saveSettings(mySettings); 168 | } 169 | 170 | //Do the same with the page 171 | if ( 172 | !isPageEnabled && 173 | !mySettings.blacklist.includes(url) && 174 | !mySettings.websites.includes(url) 175 | ) { 176 | if (!mySettings.websites.includes(url)) { 177 | mySettings.websites.push(url); 178 | } 179 | isPageEnabled = true; 180 | saveSettings(mySettings); 181 | } 182 | 183 | document.getElementById("site-switch").checked = isSiteEnabled; 184 | document.getElementById("site-label").innerText = hostname; 185 | 186 | //Set the checkbox to the correct value 187 | document.getElementById("page-switch").checked = isPageEnabled; 188 | //Everything from the end of the host name to the end of the url 189 | document.getElementById("page-label").innerText = url 190 | .substring(url.indexOf(hostname) + hostname.length) 191 | .substring(0, 25) 192 | .concat("..."); 193 | }); 194 | //Get whether the base url of this site is in the list of websites 195 | }); 196 | } 197 | 198 | //Handle the saving of settings 199 | function saveSettings(settings) { 200 | chrome.storage.local.set(settings, function () { 201 | console.log("Settings saved"); 202 | }); 203 | } 204 | --------------------------------------------------------------------------------