From d47d87da8246cf4178d55a50e0f42c9333e005d7 Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Sat, 8 Aug 2026 17:09:43 +0200 Subject: [PATCH] initial --- README.md | 51 +++++ background.js | 79 +++++++ content.css | 8 + content.js | 327 +++++++++++++++++++++++++++ fonts/NotoSansGlagolitic-Regular.ttf | Bin 0 -> 83444 bytes fonts/OFL.txt | 93 ++++++++ icons/icon-16.svg | 6 + icons/icon-32.svg | 6 + icons/icon-48.svg | 6 + icons/icon-96.svg | 6 + manifest.json | 52 +++++ package.json | 9 + popup.css | 114 ++++++++++ popup.html | 24 ++ popup.js | 83 +++++++ tests/background.test.js | 100 ++++++++ tests/transliterate.test.js | 25 ++ transliterate.js | 57 +++++ 18 files changed, 1046 insertions(+) create mode 100644 README.md create mode 100644 background.js create mode 100644 content.css create mode 100644 content.js create mode 100644 fonts/NotoSansGlagolitic-Regular.ttf create mode 100644 fonts/OFL.txt create mode 100644 icons/icon-16.svg create mode 100644 icons/icon-32.svg create mode 100644 icons/icon-48.svg create mode 100644 icons/icon-96.svg create mode 100644 manifest.json create mode 100644 package.json create mode 100644 popup.css create mode 100644 popup.html create mode 100644 popup.js create mode 100644 tests/background.test.js create mode 100644 tests/transliterate.test.js create mode 100644 transliterate.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e63b5e --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# Glagolify + +Glagolify is a dependency-free Firefox WebExtension that converts modern Russian Cyrillic page text to Glagolitic and restores the exact source text when switched off. + +## Install in Firefox + +Requires Firefox 142 or newer. + +1. Open `about:debugging#/runtime/this-firefox`. +2. Choose **Load Temporary Add-on…**. +3. Select this directory's `manifest.json`. + +Temporary add-ons remain installed until Firefox restarts. For permanent local use, package and sign the extension through Mozilla Add-ons. + +## Use + +1. Open a normal web page. +2. Select the Glagolify toolbar icon. +3. Press **Enable Glagolitic**. Press **Restore Cyrillic** to undo it. + +The popup intentionally has one action. The background script owns one absolute enabled/disabled state per tab, broadcasts it to every frame, and supplies it to frames created or navigated later. State lasts across navigation and is discarded when the tab closes. Firefox internal pages (`about:`, browser UI), extension pages, and other protected pages cannot be modified; the popup reports that restriction. + +Glagolify updates existing text and watches for text nodes that are inserted or changed later. It leaves `script`, `style`, `noscript`, `template`, `textarea`, `select`, `option`, effective `contenteditable` regions, and documents in `designMode` untouched. Visible form labels, validation messages, and button text are converted, while text controls and selectable options are not. Textual DOM hidden by HTML, CSS, or `aria-hidden` is converted proactively so it is already Glagolitic if revealed. The implementation only changes text-node data and a reversible inline font fallback; it does not replace or wrap page elements. Disconnected nodes are restored and released after each mutation batch, while nodes moved and reconnected in the same batch keep their state. + +Open shadow roots present when Glagolify is enabled are handled, including nested roots. Open roots under hosts added later are discovered through the host's DOM mutation. An open shadow root attached later to an already-connected, otherwise unchanged host may not be detected. Closed shadow roots are inaccessible to extensions and cannot be converted. + +## Transliteration conventions + +The converter has an explicit, case-preserving mapping for all 33 letters of the modern Russian alphabet (66 uppercase/lowercase entries). Punctuation, whitespace, digits, Latin text, Cyrillic characters outside that mapping, and already-Glagolitic text remain unchanged. Letters shared by Russian and another Cyrillic alphabet are converted because their code points are the same. + +The mapping uses dedicated Glagolitic letters for `Ё` (Yo), `Й` (I), `Щ` (Shta), `Ю` (Yu), and `Я` (Small Yus). Modern Russian letters without exact historical one-to-one equivalents use these practical conventions: + +- `Ь` → Yati (`Ⱑ` / `ⱑ`) +- `Ы` → Yeri (`Ⱐ` / `ⱐ`) +- `Э` → Yestu, the same output as `Е` (`Ⰵ` / `ⰵ`) + +## Bundled font + +`fonts/NotoSansGlagolitic-Regular.ttf` is bundled so converted characters remain legible when the operating system has no Glagolitic font. It is inserted before each affected element's existing computed font family, so non-Glagolitic characters continue to use the page's own typography. If the page genuinely changes an affected element's inline `font-family` while Glagolify is enabled, the extension retains that latest page-owned declaration, reapplies its Glagolitic fallback, and restores the latest page value when switched off. + +The font is **Noto Sans Glagolitic Regular**, sourced from the [Google Fonts repository](https://github.com/google/fonts/tree/main/ofl/notosansglagolitic) and distributed under the SIL Open Font License 1.1. The included license is at `fonts/OFL.txt`. + +## Automated checks + +No install or build step is required. With Node.js available, run: + +```sh +npm test +``` + +The test suite validates the complete 66-entry mapping, mixed-content preservation, case, already-Glagolitic behavior, and authoritative per-tab/background frame inheritance using Node's built-in test runner. diff --git a/background.js b/background.js new file mode 100644 index 0000000..4802c41 --- /dev/null +++ b/background.js @@ -0,0 +1,79 @@ +(function (root) { + "use strict"; + + function createBackgroundController(api) { + const tabStates = new Map(); + + function validTabId(tabId) { + return Number.isInteger(tabId) && tabId >= 0; + } + + async function broadcast(tabId, enabled) { + try { + await api.tabs.sendMessage(tabId, { + type: "glagolify:set-enabled", + enabled + }); + return true; + } catch (error) { + return false; + } + } + + api.runtime.onMessage.addListener((message, sender) => { + if (!message || typeof message.type !== "string") { + return undefined; + } + + if (message.type === "glagolify:document-ready") { + const tabId = sender && sender.tab && sender.tab.id; + return Promise.resolve({ + enabled: validTabId(tabId) && tabStates.get(tabId) === true + }); + } + + if (message.type === "glagolify:get-tab-state") { + const tabId = message.tabId; + if (!validTabId(tabId)) { + return Promise.resolve({ enabled: false, available: false }); + } + + const enabled = tabStates.get(tabId) === true; + return broadcast(tabId, enabled).then((available) => ({ enabled, available })); + } + + if (message.type === "glagolify:set-tab-enabled") { + const tabId = message.tabId; + if (!validTabId(tabId) || typeof message.enabled !== "boolean") { + return Promise.resolve({ enabled: false, available: false }); + } + + tabStates.set(tabId, message.enabled); + return broadcast(tabId, message.enabled).then((available) => ({ + enabled: message.enabled, + available + })); + } + + return undefined; + }); + + api.tabs.onRemoved.addListener((tabId) => { + tabStates.delete(tabId); + }); + + return { tabStates, broadcast }; + } + + const exported = Object.freeze({ createBackgroundController }); + + if (typeof browser === "object" && browser.runtime && browser.tabs) { + createBackgroundController(browser); + } + + if (typeof module === "object" && module.exports) { + module.exports = exported; + } + + root.GlagolifyBackground = exported; +})(typeof globalThis === "object" ? globalThis : this); diff --git a/content.css b/content.css new file mode 100644 index 0000000..a8c1956 --- /dev/null +++ b/content.css @@ -0,0 +1,8 @@ +@font-face { + font-family: "Glagolify Noto Sans Glagolitic"; + src: url("fonts/NotoSansGlagolitic-Regular.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; + unicode-range: U+2C00-2C5F; +} diff --git a/content.js b/content.js new file mode 100644 index 0000000..d96a42d --- /dev/null +++ b/content.js @@ -0,0 +1,327 @@ +(function () { + "use strict"; + + const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option"; + const FONT_FAMILY = '"Glagolify Noto Sans Glagolitic"'; + const trackedText = new Map(); + const styledElements = new Map(); + const observers = new Map(); + let ownTextMutationCounts = new WeakMap(); + let ownStyleMutationCounts = new WeakMap(); + let enabled = false; + + function isEditable(element) { + return ( + element.ownerDocument.designMode === "on" || + element.isContentEditable + ); + } + + function isEligible(node) { + const parent = node.parentElement; + return Boolean( + parent && + !parent.closest(EXCLUDED_ANCESTORS) && + !isEditable(parent) + ); + } + + function observedRootFor(node) { + const root = node.getRootNode(); + return observers.has(root) ? root : null; + } + + function markOwnMutation(counts, node) { + if (!observedRootFor(node)) { + return; + } + counts.set(node, (counts.get(node) || 0) + 1); + } + + function consumeOwnMutation(counts, node) { + const count = counts.get(node) || 0; + if (count === 0) { + return false; + } + if (count === 1) { + counts.delete(node); + } else { + counts.set(node, count - 1); + } + return true; + } + + function appliedFontFamily(computedFamily) { + return computedFamily + ? `${FONT_FAMILY}, ${computedFamily}` + : `${FONT_FAMILY}, sans-serif`; + } + + function writeAppliedFont(element, record, computedFamily) { + const appliedValue = appliedFontFamily(computedFamily); + record.appliedValue = appliedValue; + + if ( + element.style.getPropertyValue("font-family") === appliedValue && + element.style.getPropertyPriority("font-family") === "important" + ) { + return; + } + + markOwnMutation(ownStyleMutationCounts, element); + element.style.setProperty("font-family", appliedValue, "important"); + } + + function applyFont(element) { + if (styledElements.has(element)) { + return; + } + + const record = { + pageValue: element.style.getPropertyValue("font-family"), + pagePriority: element.style.getPropertyPriority("font-family"), + appliedValue: "" + }; + const computedFamily = getComputedStyle(element).fontFamily; + styledElements.set(element, record); + writeAppliedFont(element, record, computedFamily); + } + + function refreshPageFont(element) { + const record = styledElements.get(element); + if (!record) { + return; + } + + const currentValue = element.style.getPropertyValue("font-family"); + const currentPriority = element.style.getPropertyPriority("font-family"); + if (currentValue === record.appliedValue && currentPriority === "important") { + return; + } + + record.pageValue = currentValue; + record.pagePriority = currentPriority; + writeAppliedFont(element, record, getComputedStyle(element).fontFamily); + } + + function restoreFont(element, record) { + const currentValue = element.style.getPropertyValue("font-family"); + const currentPriority = element.style.getPropertyPriority("font-family"); + if (currentValue !== record.appliedValue || currentPriority !== "important") { + return; + } + + markOwnMutation(ownStyleMutationCounts, element); + if (record.pageValue) { + element.style.setProperty("font-family", record.pageValue, record.pagePriority); + } else { + element.style.removeProperty("font-family"); + } + } + + function restoreText(node) { + const record = trackedText.get(node); + if (!record) { + return; + } + + if (node.data === record.transformed) { + markOwnMutation(ownTextMutationCounts, node); + node.data = record.original; + } + trackedText.delete(node); + } + + function transformText(node) { + if (!isEligible(node)) { + restoreText(node); + return; + } + + const previous = trackedText.get(node); + if (previous && node.data === previous.transformed) { + return; + } + + const original = node.data; + const transformed = Glagolify.transliterate(original); + if (transformed === original) { + trackedText.delete(node); + return; + } + + trackedText.set(node, { original, transformed }); + applyFont(node.parentElement); + markOwnMutation(ownTextMutationCounts, node); + node.data = transformed; + } + + function visitText(root, callback) { + if (root.nodeType === Node.TEXT_NODE) { + callback(root); + return; + } + + if ( + root.nodeType !== Node.ELEMENT_NODE && + root.nodeType !== Node.DOCUMENT_NODE && + root.nodeType !== Node.DOCUMENT_FRAGMENT_NODE + ) { + return; + } + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let node; + while ((node = walker.nextNode())) { + callback(node); + } + } + + function reconcile(root) { + visitText(root, transformText); + } + + function discoverOpenShadowRoots(root) { + if ( + root.nodeType === Node.ELEMENT_NODE && + root.shadowRoot && + root.shadowRoot.mode === "open" + ) { + observeRoot(root.shadowRoot); + } + + if (typeof root.querySelectorAll !== "function") { + return; + } + + for (const element of root.querySelectorAll("*")) { + if (element.shadowRoot && element.shadowRoot.mode === "open") { + observeRoot(element.shadowRoot); + } + } + } + + function cleanupDisconnected() { + for (const node of trackedText.keys()) { + if (!node.isConnected) { + restoreText(node); + } + } + + for (const [element, record] of styledElements) { + if (!element.isConnected) { + restoreFont(element, record); + styledElements.delete(element); + } + } + + for (const [root, observer] of observers) { + if (root !== document && !root.host.isConnected) { + observer.disconnect(); + observers.delete(root); + } + } + } + + function handleMutations(records) { + for (const record of records) { + if (record.type === "characterData") { + if (!consumeOwnMutation(ownTextMutationCounts, record.target)) { + reconcile(record.target); + } + continue; + } + + if (record.type === "attributes") { + if (record.attributeName === "style") { + if (!consumeOwnMutation(ownStyleMutationCounts, record.target)) { + refreshPageFont(record.target); + } + } else if (record.attributeName === "contenteditable") { + reconcile(record.target); + } + continue; + } + + for (const node of record.addedNodes) { + reconcile(node); + discoverOpenShadowRoots(node); + } + } + + cleanupDisconnected(); + } + + function observeRoot(root) { + if (observers.has(root)) { + return; + } + + reconcile(root); + const observer = new MutationObserver(handleMutations); + observer.observe(root, { + subtree: true, + childList: true, + characterData: true, + attributes: true, + attributeFilter: ["contenteditable", "style"] + }); + observers.set(root, observer); + discoverOpenShadowRoots(root); + } + + function enable() { + if (enabled) { + return; + } + + enabled = true; + observeRoot(document); + } + + function disable() { + if (!enabled) { + return; + } + + enabled = false; + for (const observer of observers.values()) { + observer.disconnect(); + } + observers.clear(); + + for (const node of trackedText.keys()) { + restoreText(node); + } + for (const [element, record] of styledElements) { + restoreFont(element, record); + } + styledElements.clear(); + ownTextMutationCounts = new WeakMap(); + ownStyleMutationCounts = new WeakMap(); + } + + function setEnabled(target) { + if (target) { + enable(); + } else { + disable(); + } + return { enabled }; + } + + browser.runtime.onMessage.addListener((message) => { + if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") { + return undefined; + } + return Promise.resolve(setEnabled(message.enabled)); + }); + + browser.runtime.sendMessage({ type: "glagolify:document-ready" }) + .then((response) => { + setEnabled(Boolean(response && response.enabled)); + }) + .catch(() => { + setEnabled(false); + }); +})(); diff --git a/fonts/NotoSansGlagolitic-Regular.ttf b/fonts/NotoSansGlagolitic-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1b4955dee24bebab111ae47eb7f744df5fc97131 GIT binary patch literal 83444 zcmbrn31D1R^*?^^n>CYUmdrkrnaq;RmPs<%_hhm+O`4=znxspX?klArO93fRwz60j z0cEiun}{$is0fNEiwcU!qKL?*qF==2i-3NSPXC|t?o67L7V-BlesbQtci(&W-gEA` z=brnSk#WYD34fH#*w@|DyS8`D9gGL>W{khxH!{5RgR5bu~6;)deldquc4S34b+U?Uj$Nu@-0Ao4Z7&BkJ_Ow%dic4)*GgkT-t}ofJ zbK~}z^22B2S@@{W+cNNBtDv-9CNBPDQ(+ z4cE^=dEbud?d$J*t@z0J&bejpzOJj>w)KiRVYK}YM4d30d2xLg%Mbe{?bEwSS9}*>fV5|dr=<+ z3p1Af82vw!K`mrIK=R6r?1Dr&8FxUI1NQ?|W;1XbZ~!d9kDk;pOiVIkie10Qa&QC@C!|ZSTY*q;P zct0Bfocv;X=8^UAx{LT5tX{@#^0^hWpK+eO$@D093+lI$i|gY&k1YVAIR6-MP?5w~ z-pJ}@Y{s+h#c?KF8_HFsuKf@TSKOtJ+8|-WWU56Ks!2d<0Eo`}5zJ)bF zclCIdo#*5IgfV8o=XSt4`~GY_PDk5CznO8piXX=F4zn6M;~wp`0! zXVu3-;;b3(D#Cl>K)q~hEvVyWX3RpaUkfYa-)9kpikVO@fwm8S8u)jQq_fvCA23A$ z%RLs7i52iP<8y&$>*deCgr4UB=PO}Lj{@J}XHt98ccD%HX6$eZ$K-Q2;P@2GMdvY! zvhYfvSR5<2$a6h?RxlkdZ(&MhFRtxiNzDE5>MWmqo_&?w!M@H?>_PSrdxUGanY($A zhj}Tl`cKgvf~nj5(bHc|`w7~m`T zD(Gti-^6$DUHnXbDZh%}#J|e#ok!m;W1`(R<;;CJ&1^wwU! zlpo-iK>Pdn7r38a$d~bJ*tLjWuVy!}YuNSd3+!fgw``|(!eYJ!kNgn3mp#p%V?Sfh zvKQIU`N`}B_CM?o?2qhE>_hfDKFrnZ2=y20H*qgJ0Kal8FJoWh zZR{@I#J&z+c{{IX_wzw^kPopGU&J1Se}9C}uU@GFn=t?Y+<3wwfZ zXFuYnu%GbHu^+?t+`~KA^Zb1F3w|E^DL;q33?KFi{|fsxzlFWZZ)dOZJK1mf9qdhh zKl?Lc;J5ih>@WN=_749pdl&xcfB6&apZsa|FZOT#b9R{jf_==NW3ThO*jqfsi+Lfp z^J;!8|2%(ykMqS`ftX#zPhx8BfSo4zZG3`zc!US|xrkOZ>>vCo_ILg`dyhZK{*V8R zCwV)+j$hBe$bZFOhJTVrhymV+{-OOx|DnS$#swruUk2or`~sXjP(XlR3mFuUS5f4- z02)E8L;!I!V;ENiu7#xW4DcHvsScRU`DKtX0nD#hVhLc4gXNll><7wl4DgkZc`1ID=;(M*ynh zOTf|$egnBIgEt_TXYhN-QD6n|SAf>0s>OXOLb2PWNpW;5#5Q;Ja{)6&3+80Ao7^_@$613-GHTsT{y>f;>fle--jn z0e&Ck=LGn-Aj#%VNBJK>o&n(5{6~*2QpAX z-YUS!p2)TU9)zSi0i0~?4gvmC$h!pi-H;h@8kb)a;4w%VKLFO^3^9ugti2d!Fd6&+ zc6LfX8`Iy>N^6} z_h-P*0cziK8Qcr`iwx+#=QE(|F90tBbl*!E(C1%fK+pP>0OnZ6Ue18-`*jBY1Nlk@ ze}McA@G9^};I|ok2>DtDzk_^TfDc2yA%GbhK~@GwAm0+;9FpzF_0$d46?*q6F z@@)aGf~0o?ya@770=yZL>Iz^T#n?N*pK;s(`K|z;hWrcgH=KtbVSg9k-H`t)z%gdn zdjfn9!y>;NQmCL<1M6d>O~{#r&EBz%hOSH1>m z&Y%s_CP2P{+cRi_bO?|Sz*t3OW8KR z4@vbVYy=tw$iE=w6xa;eEI>Yn;t3gBAzKB=4HS+H=&VgJeKz@iX&){arQ33KpH1Epz3MA_DA3^LB1?N{y>6W3A`E-x+m~kA%7!4b&{ZC0)GG!`Xi7nD9}v= zvO5JAARnoa1h7V8$d}7tDoCXO=?D2B8B7hC0r@0_Mu68qY6WPlV~;@w@{mlCD?m1= z$P?fmNccbkk3xn7cmy&mKt4%9p8oV_rOmK=YgTLIJD+nfDd}>{>A|T?1(D^3rnw{ximWW&xgrvHy# z*7;J8jii*Ji9->d-_^6WC#CKg^QROg<4abKp|E@3m@hRlf>X(Hw=dN|M-AiSzJsDP zY81h#%qL$eLDv&h^0ARIA9`cov@fL_8JofxA6?VYQ5_xCO}VGW$H(31tyEsu+7w$d zmSO``*pJWdfs~hyyaUq@8QEGY@sNtmjE}FI9#8So@$pOx#(nG1o?z#ASxOc6_4rcC zl4-O_(={@d(gZtG+F&O%fLo@@Qfj%o(G$LP2Q@RDKDtQFb&KlL-;^}fvo@uQ`Ejw! zx6ii^?>d-Jm7s?fk4=rZrXvJ(iyz9-v#y@bn19xA za^v$2DNj6wm!b+?X!?||XJ2rdlqz>NbCW_+J~t|vttC_&obDE{$UnjDsbbuPn`fIg z_a;5rf3Wj`d~7@SKt=ALe>?^wHpCA~Qcr5#baz?G7)KL)zLcSBA+-z#&``=qpO)a0 zQP!6U4>HP~;zJ*-Ma5I5t|{NXDPPKjZYxU_#0Qp*9aOIC9xqPiuMeJ4mNLf&7LN@q z6(`+(oHxtoE%Ae_plkWq!GeOW6rb))nM!HM!L&LL8t9J^e^T5AT_{RM#tzarM0a)W zgNE^1W6U4KZP{bk16`+EFp}x&I9k$&`uE}Nd}WH6hD|lvNVp~5dEK0 zPobxSzVZ}bO{1kO6`ym_Hhb}8aj|U9#n9}Q%k@c%TiTY6(7{Ad&uHam9c{= zZtWR^QzR``&*`2Mm8psQYUP^O;%VYJJ)d+F@IJ?_EuCjJ%%b?$-4<*(Si`N<);jbD z8gSfurZ8Bh8_H7k@p5}hS*qbb6@_81Me#arlR0@0mu`;QxcY(+%z*-q4G7Ldb8y^N#ZpRD@iz8cqbtP{H6ZgB(*6 zJaL+4E2@M7e>0x+=(}JnE!n>Cp$4i@rZ0jm=%&`$ODP?kiO<&??4y^G7TdDd=2LK8*Ae)eQD1N(omV(M@5V=ci3w||bk_{TH#?1KYtD$VxF0(@#N^^?x1 zUox$k?Sg)EfmIB9L?;;G=JHe>M)|@MoFBmBxW$~RN14HRsu5xl^c-I~xn6X%S*{lyZISCmM_c83(a|=!UUalw zt`{Bckn2T9C!vp9W)-+o{*+4M!pY*W1Bbh))2Udh6Q54OT#G|0bE-I`GM|&n&;=@U z8g6c#ed_7*C;5&u#39{rrZ}YHyYZY>@yN5pAr&}V98!UEP_}LMd7qa*$$E z1ulyp%9SJAl-hkrDJgoeXu@1J-dUQ`u1_h7N6yGj=w%EGz#lI`ILWWXTql-%I}b8V z=K~?T#A0;xO}EydmUQAcsJ1HQi+|EZ>693Vs?@$7x`(QD@WHA;5 zmyFTpb=?Oe^!Xtz6DL^rxcgw3PCcx}QdOC}WbHD!BreduZ+g@wrKFEN#E)Eh@hN9~UOMuj6gcuBcI8%Lr?6i>|1MoQVwC9GyL9!4 zQGQ;GW&hj6E@6*he|iQCx!_1)@UYGyPKYTnnb(WbQT z=S=1NRM)M$QulW5?%bF2s`9SQ`!K&M|4;fG^luw(F}z`HGCpQD z&+?RY#(Kc|d)q?WgZ7C1Y=_oyw&VBCHO{wPEv}nfPq~NOH@IIcY%08@@M%xnbFEkF zUFA&`^%mV$^e^9p??K<+{jUZN1pXSlDfn7(L-BV@LM8Wxejm|Bu8lgPb9Q?lZ@%e-XGVuYScBS z)%?A-ysob9@Ab+0^Xp%0Xli(>@sg&7rhA*-ZVoj!G%svk)x4v5PxIByw=}1kf7s$` znQGb9vbSY_%WW;sxBR~4-Bw3ythJ?esCBA!SL@!^>ssG#eXp&lZK^HR_QSSk+ums3 z)V{y{`Sy>Jc}Z_Fk?c$^OKwQ+O5UD)KKWYm;|_g?uOr^Es^hwjr#fElc&FpTPHks* z=Ubiccd5G^U9ql~uA#0&T~Bng?uFg^yKn1$u=}wdTTi5?si(JRNzcxn^LwuBdAjFc zy}I7Q-tyjL?~>kiy{GlQ+WXhO!oDqiXZKy%ccAZ3-)jq`1(pS&1q}-pE;wz$B@4d1 z;NJdFe^q~5|HA%V{b%=Ivao7l+rotlUs?F}!uJMT15*Px40a9<4L&va)*|Ji-bI%z zx_i-&7kxCO910C>8#-g?zM+SPo)~&==+$Azuy6R1;YWvmJe(f!jw~1%8F_W_#NrK0 z*pkjAyO-=;^8C^zOV3`qcj?PZf4}taOFv$wU1nM4TXy-fo0i?Z?1^P>Ebm?Z-13h{ zSB*Y5`tgd$inCWdHL=i`L({{#)yx-w@eQvmv=*aKkGb3pa)~ zp1tvsjn{5Gu<_nab(^+q+P~@VoAWkLZN7W+gPR}Q64`RmmM6B_wvKH*u=U=p4{d#7 z>vLOQ-TJ4k?{6#E=G_+CHnHu_ZHKl!y6x$0zueBYcWxirKCykn_S?4KxBZpvZ*PBZ zdwNI1jx9TG+VSX)mv?-4Qo%{vPC9VXi#uP0*Qvs+_d|?s1#*qCWEkTa2~)*JCUGvU zV3Ncql{lh}J&jtWL@|X)rM8z=oBXB{zsX<4_ool=MEW`D>BDuzr^DlpERa^Shosk- zmDL*kE8j|0Q%rs{B6;!WDw28@El++4^)-K*w>$g6~Z_t5syY~~MdhgxR_p0LYhlld2Cce! zNq?xWMOs~A^Axrgda7zovwig--e+Rx()$KdIU{397v3dNQd?FA^_N61#akC6H-;=( zvPi`-Jt4CDm$e!&rA= zjkMZWTkY{Q7rL7eM-5?~_#Nz~kd-DQ(5xCNX3CAIs%eo{qf#iq?n4<|FZ7#&79dCC zf!W|4)h7H01zSzQT69>@g#Q9+t92;S^xcW?hGWJFv*((LYdjY2ZVvNB=?9FZ;q)I4 zdo1badAY@NnEH*);Qjx^`#mg?EVJioHA>j&paOk_3V;{Mrc^Mu0`O>Aq03~{<)~H6 z!#!$^t+W~zRf{^*6YA>d(4tmrR91_v+HZZFD)9Kk9r*MUIZD)wO=?Az zydKVKD&w(YzbERA+RO%ht`?+dw{Es_HLxBsA5?Aj(4r2O;3GN=on^Av{IIdw>}RUA zF&4h3Iee)Q;pW4aUTX1-dCX@1)}Tt}2i+_}y;^3pE$T0sm6-d|7U;Z@x zmD>dUDNaJ0eBfl&vHoN)=SnU1{UoMUN?PceDK#8ASF2@0j%ksOYcz_<9E@zzekro% zmF2Oz(z@aRp5d!0t};b(HSW@CbGcMY*5+4Ki-w}5e$q;9Fc1hU%w}q$#b)ExvVo9s zSZoh&Pfpg96!G&f99Nf?{|F6Ezl=X?)VZpSmFr(}>Hyo|bD3)J=rv6+gnVJrDhMUiWN-u?iB{Q|zd&jJP(bz{Y zX#NKp#v%Wc$8z|OP&1VK@2r}?;jz3X+Rj-e;<+K|MdoF_$u0|5s?1zL!wC)=o_i41 zEx{jc#HdnDGnLA^h+OKl8pdp+@#V0Y4EcGvI;|StqVQ_aYW^c{RcciyI|CH|7=mWETlxNz`GI=x{i*L3Ng;T^l}h5?Ih)vX&o zch9O>9X+Yo%__l@8(`zfQ438?gG#PYK`Nxpc{)j}<&$c8&>3>|ikuuasmT}G>S(X2 z!Xm74lbXJjx z%Hi_W*!c-<(x6SNgg&vQ0N?mNY}t!VvpSY!J;_e^AD+WCYOdJ` z4N8-k$Ka=PB@Epuo$yNKWt=738tW@7%IeDNVqssA!*0@}SG?RaYs(n5@J!+_O>Y)B zR9OTS)oqwTgm+R@V<5|eR(4YI>5L&i0{8Ti&sR~=5DGUxY^>`I8}c06z2L@5z<`~hXw$}G9&Y+|Q-t5FjyAM9jV4&J>WDmg#SoU;thg6#cC!Fj zq_wO&*`eZGrr!X?3g&r^sexGeQS zf4;_r=}K0;7`wBhwl=7j(A;tfQx^p(K^2YO?0}s)b#!a3itiR<^{H^Faj~NIxD&DG82Wzou5WpU6ODT?5!9$T>`WHQ1#`)jLfL!q#UylN30QNuByF-2OdtsH?3 z+%$P-Ked{UaF99Tdp8!98!cT`3x<7)Ym0^#RCig8Wks9PW06Sy!}$^3+#Gl~$cLig zyzc~u-%%G@T)u`4J%j4P zvEqQ&@+sYwFNr{7RcLjs4Zy_*(rfV}z zIaIYF;9FO9_uci=-lBE&hxiv!(Q&G9`e9OXpaa#$TnnG2_z3k5gNI$8tjXbE`NO&u zn<=JX2;Hg1xmsAfvOtMOR+dL2C>*XfnM}57b4{McD@T_iZn9Fvt1)iLL#Z_sVdB4R z?8Mt7d9=bs&4_5}`bZNdc;Cnst0%6XkfSC^IWkk8Z@crXnRJ)kj}m{`yBh=)~V zV{)C$)e1QJ6NDaS^0k6_> z)ka2+O&<1oUA{uAWs}N0Q~)t>XR@}Mv;Mv$mg7wg)$O(IiLzKU81TBSW@CPi2H)5U z8^pv%aoIc%FV_scDCRpF?{EiZQa#S7t*AMT{bRj7yh2KztPY`rFKG@Rl>3mTyBly~ z`UZ;!!RA|X{9>_4etFuv#2jhLdi`+oGvpWLN4)zkZss>5Vj;J0@jQFf@soQLYk{{A zzl2#?GM0m78Xtu5DA+U{BC{^is44n}vqvw&Vu~&JuSBaUBzwWEUS+dqLKE3U#IT)v zIFno4-&WOav2;~09QI7)g`=UUenKCPM8f&}x8c!&ilRc7zjJ3#I>>)pIS`NZRivH# z4;2fdrGu5}An8uFZ~g&vXMl&niU}i?!mG`+QW2vOA&jV_(`GWFn88+~);Q+cv~0i512RMprYqS`AVr@lEf?~K#I0r*EaXA_nvAK;r1ivyHtW9{#+>)`@jAKZD zPqtD%Dm#M$1lbw{V?UNB?)iF09bpMDMmYt7Wg!`9cAZ*aL?mrDx(nP|WLlVk>nW1X zu2pIEKwT>@6v*qMJ0Fk6^b=!OF16IxUvqKL)U>X{h(5zY^@aYkhR}h-k>c{jH7}Zx zw3YQQy^cJC4Y}G{)|_nc6ritE@@j5&UYJI=BD$vq55>@!7brT4mzD(4fi`A~YUDWq zt2cSzcqPnw=3WHv7@(;q|&6U&+Y16BFkSm-G(t^lipP3wwu5fpGVZ zWXJZ-*pBbeycf=Y+BLkYylU-|%F4x4iQY>hntaV69!PYxC7cUudd5mCCfXYY|9o`X zK)Yz(IJ)j`OEzn0^_ekcojM2e1!fPe@PqMK5tiQ7m9e@w)})?jQPg2K8^z)~cec05 zn2)iF&lKcK2eMZ3X(9b*3ghKiMy>_q5f$?kO*8FU1kB;ALlez58Gx`e#u?82T zSA_Dx?^RXkhm*-+eMJ@jB{ewx!M#D#X^%gQbxZo+_hJ;AcmxTDhiNRI6Y$N@SQd@u zH@_AQb}!=am)kOf(B((m;Y+AMiF_&TJZ8?y_M z+L6Clg?yn)0Yg;Pk|EaB)z?;AWm80eS}U7iSYeVK4xgU-e0$ruQ{$`8?d&*r&DyD{ zwc|5WQ(M>RCcm;_!%b7V@$sC=TQ+XEWh!Ug{rWHO-hG4q@b{&p;kw;tei_ZggRF76lT5j%T19}H}VaX*1+^9oi#k0(1m{My}za^;ImteZj&2ff`J=on5ZasL8Bz&l@Jq>$A{~**c5aH z7sxXRUC5pb-nF)-URH{IPHBGSN!Kh`aLvxG*9;6?v(@VH*v*BW6^dked50u*wkPqo zqr5z+=t?HNhjU0N~%S#c)b)Dg!KA~Bqjm9+I#i7_4~aV zy|z4=EH~zRu3EqDnkot&2sCfNt4T+oS3ky8OR^CWgj&Vbl;6lMjIm%snnxqNMsa|% zXgE+_T<&&QEpoi%=f2Dgcuc&6x#Vb^navDj!y^$9J&OtDQ7mo|XKwYH!_7BCvv;6s zM}r@%eh=Ru!ppQn&fAEPCtLU5%XN>lhGeZ0VLg*p!1Tx%Rfq;D{i)Sz;0QFD0u7oP z4F>~gsy}E(&}lBtr8F#bR8J}~RYPT&ge<~1N`wZ3w4j%bQiMXk%QX!*MNnHgCp!^t z9yaEu-_3;gKPc%hLhS7N7{MxHVvFbF^xI7l&w|JghIduAdOcW_U;`54$c{P};d`9u zZ^WvYr&S1k)M^Ra3zUkGS0x$|bNGD-ql4$g9JYu#pXse^_MK1g+!ymm%CZ7%RzFFI4Is&V1#{u}O{HoBqgJsK~dR z87Mfdru^N8tu$d8kneT4ZPr3*;o%#~YvjNq66isOS1J2~RcM0=9?FRrYGJaE<>FiT zxw<_}B5y2dHfgyU3+`Ms+bGh$Sazzg?35SZjQNY)E~mqek%7e)7FhJpk}of~p$)&e zTH_DHMNsd<=OU>PMv`H6KIO&&SNenNCgS|7@rmow?>jB&vcEmSi~bgWeBXBvl48~P zwfMM?pMBCv>AgjxiLddSckI}a?w-gl4Kr~v<;nP`tQi`l40E|0S1 zPVSf+fy}WJkuVR+nQ}_iU+XbXz^ExTwdwG;uwpvv@P!r+bs}6C`i=(O4OyFIUF=?& z-}_Au*J5GnCW$y?Tt1nsWHeW6cjc)h33C~S;O2a;VLV48+lyinmI8Ch$x%+`B2dLz z(4DM@U#5FtYWR&2wh>*bZ9|B2vhGy&Ti%IWusU^7(Wi}_=47DZU5S#po6fOH_wBUqoFNC9(44Aa|Ia9&eVQd3iM z+c8~UGpD0fxvE%mb1c?${BE9`rx1SNbo6fy=(={yv)Jij64p{kt!zA+S`nKD+H4G+ z9rz}Ft`2wRROe6}O+hoQJ~{Vi+?IwCr*;hdN?w^?=$v#F=JU$KPw=V@YLm%sGOFME z=$gNNu;~iv3x!2Rg@;e(=e(M4wdNoG@b%YUCqH}SHR(y|9mG$S?9HSZLs_9w_&m7^ zwI%@dsuhC+DLphF#4LdzU?=uBz(c_%A~t^!O>W7Ng8+mzNEu+C(TJka6BosAn2l%e zP&3+?w0_oSC&TnuexY0e{7Ncq$v91K&oDjS;E_%p_!dD|Cyt=0j5>44s)-*jbvZr(s3cJ=VuiY=#| zwmq>f(4kPSxPG!?BK*=>F5}{MNgDm)RItl;gk9k`k6)79HMncvKKw~i%sO-AK6wvP zk2%513X@Ko(SS^3KN`oy`V*PXSd9W}O}o60hY(v{Y-&{X@|GvB&+OpIi_9009m#bc z{`2G4V3Fy$-Dmb(yF_{zE)e4JP~Z zu?ef^k`hZ$`~(ZrD3J$|u#(eA_|~CPshEa^BJ4v#EgR}_=62GQRA3~Zk1brEh-?tz zg|YD=(jyuAfmmsD*$U&klXcPKA7FG0II zy6=2tc;)3IHp_s)zI(^;o=Z)+Lk2x;3tq%?* zl-v_G-VystyRb;s$XijgaiN?>GtdGgw-C6>!KxdMOfIS2^Ph^s$#51b4)}dV9=FYc zjXLbhvQl138?SPUkH)GPnkQ_$vvRlx(BeOBShYtyY29E|W_1xJ5i+GNNzuSw13) zT*_?7@txUkmSZ`JCdjGG@s0m7x&QGazU@cvyDaI!=O!L}?)33}nTSqA5!o2xwbx!d z{Nmgg0(*sum!P{2Hj|tzvEXL~LuL>WYKan2hg>K23lWSe@^x64V$YD0Nvdh8zjaYA z4U6eKJ*EVC->|I}xyZ_j^0Ig=67+it=j1|onS<@E?IGaOV z%t{M+?6&3ZxSDsv_JXukgkOiiN7E9IW&i3}5qg58mLQ{ck&xV{4 zUH(<=}o(A&n=$F^gFU_nEzl!nPK*|XlwneyF5#>sC0d~$CumRTyeX0iCvln#Y zLLM$ixPTalZi`@*rBLQ0I?6#diGElK_<<2_gI(;bn1 z3XjL4`t^#U=G7gwq@0#e*O=msH&>4ZyS(1MG^`tWF3J{KOq6HPq5I!OhLAE1rx$xR zXR?OR97j?7uA@mryZNP>#pQAm5##U3hsr-fZ)KP>Ygv;0QmCN7%{9tGuF}M~&REK^ zqXeyGPv*tA0VgG#%xElu-x_KPq`xv=R*==7ytWYWEPTVP%Ag$0-0^c9f1zBm^5j}&`W?87YlQ2#Wk4Q7 zq^7F=%n%B878Uh=Ts7aHkdkOUlJP4qWRw#;I*9mP^Slh@f<9DCAdgXu8KR0^GLSN3 zP?V~%$FIYLfS^H$ri6UCd-Jk$LOfGix92kyA5R9$;+!Qa;#Fl;u}G-I zUsULJ*enHxeDH>pte6+;vwcXGYz1#=c7C8-2c3YC>m`LCJDA~!(AoT@Wm{_%29IST z($uJ~-!gLY8M;JsXvFL_NDW*6%xg7$1B3n7F78vLevM_e%Fq(&Pq34dG^^?ihW?I3iE{zebdZ(*<^B+Str3(l@k2!Al5_-d2nQT z5b~oBuohBdE>Y)f#_XBNiDUMhokGgu0Y9kl9jz_3)$!)C=5WYg8YsoneI~m%KRcDk zLw)W(pdI#teKlIE&e;$|W@l~!*rqwU0vfUv6*?S+MK-?9?K4_6U)Za3IRcJDp_2Sm zUMS=b8I2*V?hS@eT`IpM6bj|-DoMKStwr88yE|F3YgeGdlkYVSJ1Q-{HvcZ3_LiEk z{T?g&og-*41f0%*K_7Jfl+F}k%?VBm0u2NDNyUni*zL_`>J(V+ik%=8Q<+Rkd4~ma zi|LztUqg27aGmVSv0nWu?$uy@pUXfh&Co}ZVa z#T^=}3bDQ4q%v_VlexbHG)bvHt>XWlz>fr0rJt2i{BLPDe$g&`xQ<`;#q?$SMO;L4 z>%Fl1LAEKG6A5Eq8=IgAhiFc4(HtsGW1Gghh_Vad-zc?;%q-xYa}my?AX7p~OKD8C z<4NR#+uK@Onw!dy%ePyR${XZ^lu^M98w%IUVO?!aIS3pS^iVQYub|8j?AQ*ngS_q~ zdPlhwMt+N~c>%Th`SxzNVpLre@H^d+sJnRSm76zTv9y?uUA|zEYE<%ex7H=y?r79q zP&v^*(Z4cL;EqPz?si=~S!i&T`EAZbb^Az7#musrnq@N;H6!iS38yVk?lKf6T9fRjd|;XqTi5TlFkJ;-SY`9qIn({5iAG6MJ$%N zh+_J}(t#35*)fcvED!N+jmLNY^80-r&1jzOtmS9f^4|1>Ew8_B347jr#%6l&J(KMj zie<;}?yK?cLgFFXbjYyI@(|6(@DSZjqajBNpIpcbGdx6@hKL#v4H1HN_%bxa8%rId zH;tZc%^lVFiqjj-2L7#*YW~;}rho69T*+kn){Cwpy)ruE(SaI)_yhG&BvB71xDnN~ zDitv!nTq*X{-N82@lhTP`@OD^JA@tJyc{*9douh((9-0EsbC)p9wJuWCng`Fu8z`& z;`VL!IR4t>hhNxfFb@?Njr_|y@wZ^eY=|WIrbHx?NMFp)l;sVHNFQJ8%FlPDpK$8+ zPToStq))L|^$ObJVgXk1pLvJoV|a(fFpF3?SW#TzEyOci=3td6Eb|U!7e>T@Y!)6` zM;5d;s4*iQO6hAkX(NX#_;AzEl4ULfAKEmmat9uW+dqukHxD=#UT@AV=Zm)*3;NAQ z<1_Yi2Kz0>^#3~T(TX=Ci7h|m#vd4+^-EsfOAT7>l-E2$Kh0xxi7=XlC?#9H)O^{cc7ij&%!Kxm^N?tMBH(JbcW^` z&HbU~(S+ALjchc3sGn4~)ex`a%Nk;l#Ho?QO{6)##Fd}Es=l-j8=d23Px@CLi{81@ zm47|86p7m-AMzha0j!5V6PM8b|CdYn&GIgfw>LhtN|pW>GTy_3o*uW;GZGm%rK9Ie zok2gCSFn8Lg2jVIU0;4d!|rvHXE)CoFL#3>*N9mZY%0@*{Fg zZRthgu(6^s5-#=^x*RsE8C*!BLdJP%hD#{M6D6jJNl0XaPY?-f;Z>ozPtriMJVJSh z{Ug7(!(y1&V7G6WFjzWz_3xAB#K5}%gNz0n|1J%@f}NQW=Zp5WN2Be1Q6I)0#vJr_ zX8K}`J&d@ubH`r#X3`(_kl@dzVW)0JERz=2b6oBq{OUaJU<{#XOH*xiS!uK?Ruu|* z3o(O$t%IK`c4h5Uj+x{?!cLD82Qx_;8iAAmqX>(#%|tAruP~t8$368nd9>X&5MrV3zopQ#ACsVk_w8np>R8%U^UkUj`9WV z@?gx2!&-z%Sa_52kHZxlTRpl%%NwbEhvJS-&GvE4ZdFy8&00~d*v+q8NSfSclpH&E zMk>&lNaD*UUBDZ9m!&_qYWVANOZE!?BI`{tg!Z_QBa34t1B

RG-cdG$VSM!w)Pk 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 rV2$o>tU)~i?IiT~Lw0Ga6?UPo4 + + + + + diff --git a/icons/icon-32.svg b/icons/icon-32.svg new file mode 100644 index 0000000..75a6643 --- /dev/null +++ b/icons/icon-32.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/icon-48.svg b/icons/icon-48.svg new file mode 100644 index 0000000..75a6643 --- /dev/null +++ b/icons/icon-48.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/icons/icon-96.svg b/icons/icon-96.svg new file mode 100644 index 0000000..75a6643 --- /dev/null +++ b/icons/icon-96.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..f304777 --- /dev/null +++ b/manifest.json @@ -0,0 +1,52 @@ +{ + "manifest_version": 2, + "name": "Glagolify", + "version": "1.0.0", + "description": "Toggle modern Russian page text between Cyrillic and Glagolitic.", + "browser_specific_settings": { + "gecko": { + "id": "glagolify@example.local", + "strict_min_version": "142.0", + "data_collection_permissions": { + "required": ["none"] + } + } + }, + "permissions": [ + "activeTab", + "" + ], + "background": { + "scripts": ["background.js"], + "persistent": true + }, + "browser_action": { + "default_title": "Glagolify this page", + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon-16.svg", + "32": "icons/icon-32.svg", + "48": "icons/icon-48.svg", + "96": "icons/icon-96.svg" + } + }, + "icons": { + "16": "icons/icon-16.svg", + "32": "icons/icon-32.svg", + "48": "icons/icon-48.svg", + "96": "icons/icon-96.svg" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["transliterate.js", "content.js"], + "css": ["content.css"], + "run_at": "document_idle", + "all_frames": true, + "match_about_blank": true + } + ], + "web_accessible_resources": [ + "fonts/NotoSansGlagolitic-Regular.ttf" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8b4537 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "glagolify-firefox-extension", + "version": "1.0.0", + "private": true, + "description": "A dependency-free Firefox extension for toggling modern Russian text to Glagolitic.", + "scripts": { + "test": "node --test" + } +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..9b424d1 --- /dev/null +++ b/popup.css @@ -0,0 +1,114 @@ +:root { + color-scheme: light dark; + font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + width: 292px; + margin: 0; + color: #22202b; + background: #fbfaff; +} + +main { + padding: 20px; +} + +header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 18px; +} + +header img { + flex: 0 0 auto; + filter: drop-shadow(0 3px 7px rgb(49 30 93 / 20%)); +} + +h1, +p { + margin: 0; +} + +h1 { + font-size: 18px; + line-height: 1.2; + letter-spacing: -0.01em; +} + +header p { + margin-top: 2px; + color: #6d6878; + font-size: 12px; +} + +.status { + min-height: 20px; + margin-bottom: 10px; + color: #5f596a; + font-size: 13px; +} + +button { + width: 100%; + min-height: 42px; + border: 0; + border-radius: 10px; + padding: 10px 16px; + color: #fff; + background: #6842b8; + box-shadow: 0 5px 14px rgb(77 45 146 / 25%); + font: inherit; + font-weight: 650; + cursor: pointer; + transition: background 120ms ease, box-shadow 120ms ease, transform 120ms ease; +} + +button:hover:not(:disabled) { + background: #56329e; + box-shadow: 0 7px 17px rgb(77 45 146 / 31%); + transform: translateY(-1px); +} + +button:active:not(:disabled) { + transform: translateY(0); +} + +button:focus-visible { + outline: 3px solid #bba2ec; + outline-offset: 2px; +} + +button[aria-pressed="true"] { + background: #34303d; + box-shadow: 0 5px 14px rgb(24 20 32 / 22%); +} + +button:disabled { + color: #a29da9; + background: #e7e3eb; + box-shadow: none; + cursor: not-allowed; +} + +@media (prefers-color-scheme: dark) { + body { + color: #f4f0fa; + background: #201d26; + } + + header p, + .status { + color: #bdb5c7; + } + + button:disabled { + color: #77717e; + background: #343039; + } +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..9b96fd3 --- /dev/null +++ b/popup.html @@ -0,0 +1,24 @@ + + + + + + Glagolify + + + +

+
+ +
+

Glagolify

+

Modern Russian in Glagolitic

+
+
+ +

Checking this page…

+ +
+ + + diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..9c62d1b --- /dev/null +++ b/popup.js @@ -0,0 +1,83 @@ +(function () { + "use strict"; + + const status = document.getElementById("status"); + const toggle = document.getElementById("toggle"); + let activeTabId = null; + let currentEnabled = false; + let busy = false; + + function render(enabled) { + currentEnabled = enabled; + toggle.disabled = busy || activeTabId === null; + toggle.setAttribute("aria-pressed", String(enabled)); + toggle.textContent = enabled ? "Restore Cyrillic" : "Enable Glagolitic"; + status.textContent = enabled ? "Glagolitic is on for this tab." : "Cyrillic is unchanged in this tab."; + } + + function showUnavailable() { + activeTabId = null; + currentEnabled = false; + toggle.disabled = true; + toggle.setAttribute("aria-pressed", "false"); + toggle.textContent = "Unavailable on this page"; + status.textContent = "Firefox protects this page from extensions."; + } + + async function askBackground(type, extra = {}) { + return browser.runtime.sendMessage({ type, ...extra }); + } + + toggle.addEventListener("click", async () => { + if (busy || activeTabId === null) { + return; + } + + const targetEnabled = !currentEnabled; + busy = true; + toggle.disabled = true; + status.textContent = "Updating tab…"; + try { + const response = await askBackground("glagolify:set-tab-enabled", { + tabId: activeTabId, + enabled: targetEnabled + }); + if (!response || !response.available) { + showUnavailable(); + return; + } + render(response.enabled === true); + } catch (error) { + showUnavailable(); + } finally { + busy = false; + if (activeTabId !== null) { + toggle.disabled = false; + } + } + }); + + async function initialize() { + try { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tabs[0] || typeof tabs[0].id !== "number") { + showUnavailable(); + return; + } + + activeTabId = tabs[0].id; + const response = await askBackground("glagolify:get-tab-state", { + tabId: activeTabId + }); + if (!response || !response.available) { + showUnavailable(); + return; + } + render(response.enabled === true); + } catch (error) { + showUnavailable(); + } + } + + initialize(); +})(); diff --git a/tests/background.test.js b/tests/background.test.js new file mode 100644 index 0000000..aa039b4 --- /dev/null +++ b/tests/background.test.js @@ -0,0 +1,100 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { createBackgroundController } = require("../background.js"); + +function makeBrowser() { + let messageListener; + let removedListener; + const broadcasts = []; + const api = { + runtime: { + onMessage: { + addListener(listener) { + messageListener = listener; + } + } + }, + tabs: { + async sendMessage(tabId, message) { + broadcasts.push({ tabId, message }); + return { enabled: message.enabled }; + }, + onRemoved: { + addListener(listener) { + removedListener = listener; + } + } + } + }; + + return { + api, + broadcasts, + send(message, sender = {}) { + return messageListener(message, sender); + }, + remove(tabId) { + removedListener(tabId); + } + }; +} + +test("owns absolute state per tab and supplies it to new frames", async () => { + const fake = makeBrowser(); + const controller = createBackgroundController(fake.api); + + assert.deepEqual( + await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 0 }), + { enabled: false } + ); + + assert.deepEqual( + await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: true }), + { enabled: true, available: true } + ); + assert.deepEqual(fake.broadcasts.at(-1), { + tabId: 17, + message: { type: "glagolify:set-enabled", enabled: true } + }); + + assert.deepEqual( + await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 4 }), + { enabled: true } + ); + + await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: true }); + assert.equal(controller.tabStates.get(17), true, "repeated absolute enable cannot invert state"); + + assert.deepEqual( + await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: false }), + { enabled: false, available: true } + ); + assert.deepEqual( + await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 5 }), + { enabled: false } + ); + + fake.remove(17); + assert.equal(controller.tabStates.has(17), false); +}); + +test("keeps states isolated between tabs and reports protected tabs", async () => { + const fake = makeBrowser(); + createBackgroundController(fake.api); + + await fake.send({ type: "glagolify:set-tab-enabled", tabId: 3, enabled: true }); + assert.deepEqual( + await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 4 } }), + { enabled: false } + ); + + fake.api.tabs.sendMessage = async () => { + throw new Error("No matching recipient"); + }; + assert.deepEqual( + await fake.send({ type: "glagolify:get-tab-state", tabId: 99 }), + { enabled: false, available: false } + ); +}); diff --git a/tests/transliterate.test.js b/tests/transliterate.test.js new file mode 100644 index 0000000..a187c12 --- /dev/null +++ b/tests/transliterate.test.js @@ -0,0 +1,25 @@ +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { characterMap, transliterate } = require("../transliterate.js"); + +test("maps every modern Russian uppercase and lowercase letter", () => { + const russian = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя"; + const expected = "ⰀⰁⰂⰃⰄⰅⰦⰆⰈⰉⰋⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰜⰝⰞⰛⰟⰠⰡⰅⰣⰤⰰⰱⰲⰳⰴⰵⱖⰶⰸⰹⰻⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱌⱍⱎⱋⱏⱐⱑⰵⱓⱔ"; + + assert.equal(characterMap.size, 66); + assert.equal(transliterate(russian), expected); +}); + +test("preserves case, punctuation, spacing, digits, and non-Russian text", () => { + assert.equal( + transliterate("Привет, мир! Ёж № 5 — JavaScript."), + "Ⱂⱃⰹⰲⰵⱅ, ⰿⰹⱃ! Ⱖⰶ № 5 — JavaScript." + ); +}); + +test("leaves already Glagolitic and unmapped Cyrillic letters unchanged", () => { + const text = "Ⰳⰾⰰⰳⱁⰾⰹⱌⰰ · ІЇЄҐЎЅ · 123"; + assert.equal(transliterate(text), text); +}); diff --git a/transliterate.js b/transliterate.js new file mode 100644 index 0000000..130b69d --- /dev/null +++ b/transliterate.js @@ -0,0 +1,57 @@ +(function (root) { + "use strict"; + + const pairs = [ + ["А", "Ⰰ"], ["а", "ⰰ"], + ["Б", "Ⰱ"], ["б", "ⰱ"], + ["В", "Ⰲ"], ["в", "ⰲ"], + ["Г", "Ⰳ"], ["г", "ⰳ"], + ["Д", "Ⰴ"], ["д", "ⰴ"], + ["Е", "Ⰵ"], ["е", "ⰵ"], + ["Ё", "Ⱖ"], ["ё", "ⱖ"], + ["Ж", "Ⰶ"], ["ж", "ⰶ"], + ["З", "Ⰸ"], ["з", "ⰸ"], + ["И", "Ⰹ"], ["и", "ⰹ"], + ["Й", "Ⰻ"], ["й", "ⰻ"], + ["К", "Ⰽ"], ["к", "ⰽ"], + ["Л", "Ⰾ"], ["л", "ⰾ"], + ["М", "Ⰿ"], ["м", "ⰿ"], + ["Н", "Ⱀ"], ["н", "ⱀ"], + ["О", "Ⱁ"], ["о", "ⱁ"], + ["П", "Ⱂ"], ["п", "ⱂ"], + ["Р", "Ⱃ"], ["р", "ⱃ"], + ["С", "Ⱄ"], ["с", "ⱄ"], + ["Т", "Ⱅ"], ["т", "ⱅ"], + ["У", "Ⱆ"], ["у", "ⱆ"], + ["Ф", "Ⱇ"], ["ф", "ⱇ"], + ["Х", "Ⱈ"], ["х", "ⱈ"], + ["Ц", "Ⱌ"], ["ц", "ⱌ"], + ["Ч", "Ⱍ"], ["ч", "ⱍ"], + ["Ш", "Ⱎ"], ["ш", "ⱎ"], + ["Щ", "Ⱋ"], ["щ", "ⱋ"], + ["Ъ", "Ⱏ"], ["ъ", "ⱏ"], + ["Ы", "Ⱐ"], ["ы", "ⱐ"], + ["Ь", "Ⱑ"], ["ь", "ⱑ"], + ["Э", "Ⰵ"], ["э", "ⰵ"], + ["Ю", "Ⱓ"], ["ю", "ⱓ"], + ["Я", "Ⱔ"], ["я", "ⱔ"] + ]; + + const characterMap = new Map(pairs); + const russianPattern = /[А-Яа-яЁё]/g; + + function transliterate(text) { + return text.replace(russianPattern, (character) => characterMap.get(character)); + } + + const api = Object.freeze({ + characterMap, + transliterate + }); + + root.Glagolify = api; + + if (typeof module === "object" && module.exports) { + module.exports = api; + } +})(typeof globalThis === "object" ? globalThis : this);