From 57c821eb3028584f1d2407ec6d3ff4bb73163fac Mon Sep 17 00:00:00 2001 From: Tund Date: Wed, 6 May 2026 01:10:45 -0500 Subject: [PATCH] Preserve original font for private-use glyphs --- app/assets/styles/core/opendyslexic.css | 80 +++++++++++++++++++++++++ app/scripts/content/engine.js | 63 +++++++++++++++++++ tests/content/engine.test.js | 49 +++++++++++++++ 3 files changed, 192 insertions(+) diff --git a/app/assets/styles/core/opendyslexic.css b/app/assets/styles/core/opendyslexic.css index 727b323..cd6f9b2 100644 --- a/app/assets/styles/core/opendyslexic.css +++ b/app/assets/styles/core/opendyslexic.css @@ -55,6 +55,14 @@ body.opendyslexic-font-regular :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -102,6 +110,14 @@ body.opendyslexic-font-italic :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -149,6 +165,14 @@ body.opendyslexic-font-bold :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -299,6 +323,14 @@ body.helperbird-reading-lora-regular :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -340,6 +372,14 @@ body.helperbird-reading-lora :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -386,6 +426,14 @@ body.helperbird-reading-lora-italic :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -432,6 +480,14 @@ body.helperbird-reading-lora-bold :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -478,6 +534,14 @@ body.helperbird-reading-opendyslexic :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -525,6 +589,14 @@ body.helperbird-reading-opendyslexic-italic :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], @@ -571,6 +643,14 @@ body.helperbird-reading-opendyslexic-bold :where( svg, svg *, + .opendyslexic-preserve-font, + .opendyslexic-preserve-font *, + [aria-hidden='true'], + [aria-hidden='true'] *, + [role='img'], + [role='img'] *, + [data-icon], + [data-icon] *, [class*='fui-'],[class*='fa-'], [class*='fa-'] *, [class~='fa'], diff --git a/app/scripts/content/engine.js b/app/scripts/content/engine.js index f5697e1..87eda40 100644 --- a/app/scripts/content/engine.js +++ b/app/scripts/content/engine.js @@ -5,6 +5,10 @@ let currentFont = 'regular'; const FONT_ID = 'opendyslexic-font-styles'; const BODY_CLASS_PREFIX = 'opendyslexic-font-'; +const PRESERVE_FONT_CLASS = 'opendyslexic-preserve-font'; +const PRIVATE_USE_TEXT = /[\uE000-\uF8FF\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/u; + +let preserveFontObserver = null; function injectCssInline(id, cssString) { let styleTag = document.getElementById(id); @@ -29,6 +33,57 @@ function removeBodyClasses() { .forEach((className) => document.body.classList.remove(className)); } +function markPrivateUseText(root = document.body) { + const nodeFilter = document.defaultView?.NodeFilter || NodeFilter; + const treeWalker = document.createTreeWalker(root, nodeFilter.SHOW_TEXT); + + while (treeWalker.nextNode()) { + const parent = treeWalker.currentNode.parentElement; + if (parent && PRIVATE_USE_TEXT.test(treeWalker.currentNode.nodeValue)) { + parent.classList.add(PRESERVE_FONT_CLASS); + } + } +} + +function observePrivateUseText() { + if (preserveFontObserver || typeof MutationObserver === 'undefined') return; + + preserveFontObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'characterData') { + const parent = mutation.target.parentElement; + if (parent && PRIVATE_USE_TEXT.test(mutation.target.nodeValue)) { + parent.classList.add(PRESERVE_FONT_CLASS); + } + return; + } + + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.TEXT_NODE) { + const parent = node.parentElement; + if (parent && PRIVATE_USE_TEXT.test(node.nodeValue)) { + parent.classList.add(PRESERVE_FONT_CLASS); + } + } else if (node.nodeType === Node.ELEMENT_NODE) { + markPrivateUseText(node); + } + }); + }); + }); + + preserveFontObserver.observe(document.body, { + childList: true, + characterData: true, + subtree: true + }); +} + +function removePreservedFontClasses() { + document + .querySelectorAll(`.${PRESERVE_FONT_CLASS}`) + .forEach((element) => element.classList.remove(PRESERVE_FONT_CLASS)); +} + function applyFont(fontName) { removeStyleTag(FONT_ID); @@ -42,11 +97,19 @@ function applyFont(fontName) { const className = BODY_CLASS_PREFIX + fontName.toLowerCase(); document.body.classList.add(className); + markPrivateUseText(); + observePrivateUseText(); } function removeFont() { removeStyleTag(FONT_ID); removeBodyClasses(); + removePreservedFontClasses(); + + if (preserveFontObserver) { + preserveFontObserver.disconnect(); + preserveFontObserver = null; + } } function updateFontMode(mode, font) { diff --git a/tests/content/engine.test.js b/tests/content/engine.test.js index 46b97f3..eaa7fdd 100644 --- a/tests/content/engine.test.js +++ b/tests/content/engine.test.js @@ -140,6 +140,55 @@ describe('font application via messages', () => { expect(document.getElementById(FONT_ID)).toBeNull(); }); + + it('preserves the original font for private-use glyphs', () => { + const icon = document.createElement('span'); + icon.textContent = '\uE000'; + document.body.appendChild(icon); + + messageListener({ + type: 'updateFont', + enabled: true, + font: 'regular' + }); + + expect(icon.classList.contains('opendyslexic-preserve-font')).toBe(true); + }); + + it('preserves private-use glyphs added after the font is enabled', async () => { + messageListener({ + type: 'updateFont', + enabled: true, + font: 'regular' + }); + + const icon = document.createElement('span'); + icon.textContent = '\uE000'; + document.body.appendChild(icon); + + await Promise.resolve(); + + expect(icon.classList.contains('opendyslexic-preserve-font')).toBe(true); + }); + + it('removes preserved font markers when disabled', () => { + const icon = document.createElement('span'); + icon.textContent = '\uE000'; + document.body.appendChild(icon); + + messageListener({ + type: 'updateFont', + enabled: true, + font: 'regular' + }); + messageListener({ + type: 'updateFont', + enabled: false, + font: 'regular' + }); + + expect(icon.classList.contains('opendyslexic-preserve-font')).toBe(false); + }); }); describe('style tag management', () => {