add angular-round selector

This commit is contained in:
2026-08-08 18:05:03 +02:00
parent d47d87da82
commit de3b82d024
14 changed files with 447 additions and 50 deletions
+16 -10
View File
@@ -16,9 +16,10 @@ Temporary add-ons remain installed until Firefox restarts. For permanent local u
1. Open a normal web page.
2. Select the Glagolify toolbar icon.
3. Press **Enable Glagolitic**. Press **Restore Cyrillic** to undo it.
3. Choose **Round** or **Angular** letter shapes.
4. 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.
The popup keeps one action button and a two-choice letter-style selector. The selected style is stored as a global extension preference and updates every enabled tab immediately. The background script separately owns one absolute enabled/disabled state per tab, broadcasts it to every frame, and supplies both state and style to frames created or navigated later. Tab 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.
@@ -28,17 +29,22 @@ Open shadow roots present when Glagolify is enabled are handled, including neste
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:
The mapping uses dedicated Glagolitic letters for `Ё` (Yo), `Й` (I), `Щ` (Shta), and `Ю` (Yu). Modern Russian letters without exact historical one-to-one equivalents use these conventions:
- `Ь` → Yati (`` / ``)
- `Ы` → Yeri (`` / ``)
- `Э`Yestu, the same output as `Е` (`` / ``)
- `Ь` → Yeri (`` / ``)
- `Ъ` → Yeru (`` / ``)
- `Ы`the Yeru + Izhe digraph (`ⰟⰉ` / `ⱏⰹ`)
- `Е` and `Э` → Yestu (`Ⰵ` / `ⰵ`)
- `Я` → Yati (`Ⱑ` / `ⱑ`)
## Bundled font
## Bundled fonts
`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.
Glagolify bundles two OFL-licensed fonts so converted characters remain legible and the popup can switch between historical letter shapes:
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`.
- **Round:** [Noto Sans Glagolitic Regular](https://github.com/google/fonts/tree/main/ofl/notosansglagolitic), the standard rounded Unicode Glagolitic design. Its license is `fonts/OFL.txt`.
- **Angular:** [Shafarik Regular](https://github.com/google/fonts/tree/main/ofl/shafarik) with OpenType Stylistic Set 3 (`ss03`), which Shafarik defines as Croatian Angular or Square Glagolitic. Its license is `fonts/Shafarik-OFL.txt`.
The selected font 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 the selected Glagolitic fallback, and restores the latest page value when switched off.
## Automated checks
@@ -48,4 +54,4 @@ No install or build step is required. With Node.js available, run:
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.
The test suite validates the complete 66-entry mapping, mixed-content preservation, case, already-Glagolitic behavior, authoritative per-tab/background frame inheritance, and persisted round/angular font selection using Node's built-in test runner.
+65 -16
View File
@@ -1,18 +1,33 @@
(function (root) {
"use strict";
const FONT_STYLE_KEY = "glagolifyFontStyle";
const FONT_STYLES = new Set(["round", "angular"]);
const DEFAULT_FONT_STYLE = "round";
function createBackgroundController(api) {
const tabStates = new Map();
let fontStyle = DEFAULT_FONT_STYLE;
const fontStyleReady = Promise.resolve(api.storage.local.get(FONT_STYLE_KEY))
.then((stored) => {
if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) {
fontStyle = stored[FONT_STYLE_KEY];
}
return fontStyle;
})
.catch(() => fontStyle);
function validTabId(tabId) {
return Number.isInteger(tabId) && tabId >= 0;
}
async function broadcast(tabId, enabled) {
await fontStyleReady;
try {
await api.tabs.sendMessage(tabId, {
type: "glagolify:set-enabled",
enabled
enabled,
fontStyle
});
return true;
} catch (error) {
@@ -27,32 +42,61 @@
if (message.type === "glagolify:document-ready") {
const tabId = sender && sender.tab && sender.tab.id;
return Promise.resolve({
enabled: validTabId(tabId) && tabStates.get(tabId) === true
});
return fontStyleReady.then(() => ({
enabled: validTabId(tabId) && tabStates.get(tabId) === true,
fontStyle
}));
}
if (message.type === "glagolify:get-tab-state") {
const tabId = message.tabId;
if (!validTabId(tabId)) {
return Promise.resolve({ enabled: false, available: false });
}
return fontStyleReady.then(async () => {
if (!validTabId(tabId)) {
return { enabled: false, available: false, fontStyle };
}
const enabled = tabStates.get(tabId) === true;
return broadcast(tabId, enabled).then((available) => ({ enabled, available }));
const enabled = tabStates.get(tabId) === true;
const available = await broadcast(tabId, enabled);
return { enabled, available, fontStyle };
});
}
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 });
return fontStyleReady.then(() => ({
enabled: false,
available: false,
fontStyle
}));
}
tabStates.set(tabId, message.enabled);
return broadcast(tabId, message.enabled).then((available) => ({
enabled: message.enabled,
available
}));
return fontStyleReady.then(async () => {
tabStates.set(tabId, message.enabled);
const available = await broadcast(tabId, message.enabled);
return {
enabled: message.enabled,
available,
fontStyle
};
});
}
if (message.type === "glagolify:set-font-style") {
if (!FONT_STYLES.has(message.fontStyle)) {
return fontStyleReady.then(() => ({ fontStyle, accepted: false }));
}
return fontStyleReady.then(async () => {
await api.storage.local.set({ [FONT_STYLE_KEY]: message.fontStyle });
fontStyle = message.fontStyle;
await Promise.all(
[...tabStates]
.filter(([, tabEnabled]) => tabEnabled)
.map(([tabId]) => broadcast(tabId, true))
);
return { fontStyle, accepted: true };
});
}
return undefined;
@@ -62,7 +106,12 @@
tabStates.delete(tabId);
});
return { tabStates, broadcast };
return {
tabStates,
broadcast,
fontStyleReady,
getFontStyle: () => fontStyle
};
}
const exported = Object.freeze({ createBackgroundController });
+10
View File
@@ -6,3 +6,13 @@
font-display: swap;
unicode-range: U+2C00-2C5F;
}
@font-face {
font-family: "Glagolify Shafarik Angular";
src: url("fonts/Shafarik-Regular.ttf") format("truetype");
font-style: normal;
font-weight: 400;
font-display: swap;
font-feature-settings: "ss03";
unicode-range: U+2C00-2C5F;
}
+26 -8
View File
@@ -2,7 +2,11 @@
"use strict";
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
const FONT_FAMILY = '"Glagolify Noto Sans Glagolitic"';
const FONT_FAMILIES = Object.freeze({
round: '"Glagolify Noto Sans Glagolitic"',
angular: '"Glagolify Shafarik Angular"'
});
let fontFamily = FONT_FAMILIES.round;
const trackedText = new Map();
const styledElements = new Map();
const observers = new Map();
@@ -53,8 +57,19 @@
function appliedFontFamily(computedFamily) {
return computedFamily
? `${FONT_FAMILY}, ${computedFamily}`
: `${FONT_FAMILY}, sans-serif`;
? `${fontFamily}, ${computedFamily}`
: `${fontFamily}, sans-serif`;
}
function setFontStyle(style) {
const nextFontFamily = FONT_FAMILIES[style];
if (!nextFontFamily || nextFontFamily === fontFamily) {
return;
}
fontFamily = nextFontFamily;
for (const [element, record] of styledElements) {
writeAppliedFont(element, record, record.pageComputedFamily);
}
}
function writeAppliedFont(element, record, computedFamily) {
@@ -77,12 +92,13 @@
return;
}
const computedFamily = getComputedStyle(element).fontFamily;
const record = {
pageValue: element.style.getPropertyValue("font-family"),
pagePriority: element.style.getPropertyPriority("font-family"),
pageComputedFamily: computedFamily,
appliedValue: ""
};
const computedFamily = getComputedStyle(element).fontFamily;
styledElements.set(element, record);
writeAppliedFont(element, record, computedFamily);
}
@@ -101,7 +117,8 @@
record.pageValue = currentValue;
record.pagePriority = currentPriority;
writeAppliedFont(element, record, getComputedStyle(element).fontFamily);
record.pageComputedFamily = getComputedStyle(element).fontFamily;
writeAppliedFont(element, record, record.pageComputedFamily);
}
function restoreFont(element, record) {
@@ -301,7 +318,8 @@
ownStyleMutationCounts = new WeakMap();
}
function setEnabled(target) {
function setEnabled(target, style) {
setFontStyle(style);
if (target) {
enable();
} else {
@@ -314,12 +332,12 @@
if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") {
return undefined;
}
return Promise.resolve(setEnabled(message.enabled));
return Promise.resolve(setEnabled(message.enabled, message.fontStyle));
});
browser.runtime.sendMessage({ type: "glagolify:document-ready" })
.then((response) => {
setEnabled(Boolean(response && response.enabled));
setEnabled(Boolean(response && response.enabled), response && response.fontStyle);
})
.catch(() => {
setEnabled(false);
+93
View File
@@ -0,0 +1,93 @@
Copyright 2025 The Shafarik Project Authors (https://github.com/slavonic/Shafarik.git)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+4 -2
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Glagolify",
"version": "1.0.0",
"version": "1.1.0",
"description": "Toggle modern Russian page text between Cyrillic and Glagolitic.",
"browser_specific_settings": {
"gecko": {
@@ -14,6 +14,7 @@
},
"permissions": [
"activeTab",
"storage",
"<all_urls>"
],
"background": {
@@ -47,6 +48,7 @@
}
],
"web_accessible_resources": [
"fonts/NotoSansGlagolitic-Regular.ttf"
"fonts/NotoSansGlagolitic-Regular.ttf",
"fonts/Shafarik-Regular.ttf"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "glagolify-firefox-extension",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"description": "A dependency-free Firefox extension for toggling modern Russian text to Glagolitic.",
"scripts": {
+101
View File
@@ -1,3 +1,14 @@
@font-face {
font-family: "Glagolify Noto Sans Glagolitic";
src: url("fonts/NotoSansGlagolitic-Regular.ttf") format("truetype");
}
@font-face {
font-family: "Glagolify Shafarik Angular";
src: url("fonts/Shafarik-Regular.ttf") format("truetype");
font-feature-settings: "ss03";
}
:root {
color-scheme: light dark;
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -47,6 +58,80 @@ header p {
font-size: 12px;
}
.font-style {
min-width: 0;
margin: 0 0 14px;
border: 0;
padding: 0;
}
.font-style legend {
margin-bottom: 7px;
padding: 0;
color: #5f596a;
font-size: 12px;
font-weight: 650;
}
.style-options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.style-option {
position: relative;
}
.style-option input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.style-option > span {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 38px;
border: 1px solid #d9d2e5;
border-radius: 9px;
padding: 7px 9px;
background: #f5f2fa;
color: #50495b;
cursor: pointer;
transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
}
.style-option input:checked + span {
border-color: #6842b8;
background: #eee7fb;
color: #4c288f;
}
.style-option input:focus-visible + span {
outline: 3px solid #bba2ec;
outline-offset: 2px;
}
.style-option input:disabled + span {
cursor: wait;
opacity: 0.65;
}
.glyph {
font-size: 18px;
line-height: 1;
}
.round-glyph {
font-family: "Glagolify Noto Sans Glagolitic";
}
.angular-glyph {
font-family: "Glagolify Shafarik Angular";
}
.status {
min-height: 20px;
margin-bottom: 10px;
@@ -107,6 +192,22 @@ button:disabled {
color: #bdb5c7;
}
.font-style legend {
color: #bdb5c7;
}
.style-option > span {
border-color: #4a4452;
background: #29252f;
color: #c9c1d1;
}
.style-option input:checked + span {
border-color: #9d7cdd;
background: #392d4d;
color: #e2d4ff;
}
button:disabled {
color: #77717e;
background: #343039;
+14
View File
@@ -16,6 +16,20 @@
</div>
</header>
<fieldset class="font-style">
<legend>Letter style</legend>
<div class="style-options">
<label class="style-option">
<input type="radio" name="font-style" value="round" checked disabled>
<span><span>Round</span><span class="glyph round-glyph">ⰀⰁ</span></span>
</label>
<label class="style-option">
<input type="radio" name="font-style" value="angular" disabled>
<span><span>Angular</span><span class="glyph angular-glyph">ⰀⰁ</span></span>
</label>
</div>
</fieldset>
<p id="status" class="status" aria-live="polite">Checking this page…</p>
<button id="toggle" type="button" aria-pressed="false" disabled>Enable Glagolitic</button>
</main>
+45
View File
@@ -3,9 +3,12 @@
const status = document.getElementById("status");
const toggle = document.getElementById("toggle");
const fontStyleInputs = [...document.querySelectorAll('input[name="font-style"]')];
let activeTabId = null;
let currentEnabled = false;
let currentFontStyle = "round";
let busy = false;
let fontStyleBusy = false;
function render(enabled) {
currentEnabled = enabled;
@@ -15,6 +18,20 @@
status.textContent = enabled ? "Glagolitic is on for this tab." : "Cyrillic is unchanged in this tab.";
}
function setFontStyleDisabled(disabled) {
for (const input of fontStyleInputs) {
input.disabled = disabled;
}
}
function renderFontStyle(style) {
currentFontStyle = style === "angular" ? "angular" : "round";
for (const input of fontStyleInputs) {
input.checked = input.value === currentFontStyle;
}
setFontStyleDisabled(false);
}
function showUnavailable() {
activeTabId = null;
currentEnabled = false;
@@ -47,6 +64,7 @@
return;
}
render(response.enabled === true);
renderFontStyle(response.fontStyle);
} catch (error) {
showUnavailable();
} finally {
@@ -57,6 +75,32 @@
}
});
for (const input of fontStyleInputs) {
input.addEventListener("change", async () => {
if (!input.checked || fontStyleBusy || input.value === currentFontStyle) {
return;
}
const previousFontStyle = currentFontStyle;
fontStyleBusy = true;
setFontStyleDisabled(true);
try {
const response = await askBackground("glagolify:set-font-style", {
fontStyle: input.value
});
if (!response || !response.accepted) {
throw new Error("Font style was rejected");
}
renderFontStyle(response.fontStyle);
} catch (error) {
renderFontStyle(previousFontStyle);
} finally {
fontStyleBusy = false;
setFontStyleDisabled(false);
}
});
}
async function initialize() {
try {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
@@ -69,6 +113,7 @@
const response = await askBackground("glagolify:get-tab-state", {
tabId: activeTabId
});
renderFontStyle(response && response.fontStyle);
if (!response || !response.available) {
showUnavailable();
return;
+64 -9
View File
@@ -4,10 +4,12 @@ const test = require("node:test");
const assert = require("node:assert/strict");
const { createBackgroundController } = require("../background.js");
function makeBrowser() {
function makeBrowser(initialStorage = {}) {
let messageListener;
let removedListener;
const broadcasts = [];
const stored = { ...initialStorage };
const storageWrites = [];
const api = {
runtime: {
onMessage: {
@@ -16,6 +18,17 @@ function makeBrowser() {
}
}
},
storage: {
local: {
async get(key) {
return { [key]: stored[key] };
},
async set(values) {
storageWrites.push({ ...values });
Object.assign(stored, values);
}
}
},
tabs: {
async sendMessage(tabId, message) {
broadcasts.push({ tabId, message });
@@ -32,6 +45,8 @@ function makeBrowser() {
return {
api,
broadcasts,
stored,
storageWrites,
send(message, sender = {}) {
return messageListener(message, sender);
},
@@ -47,21 +62,21 @@ test("owns absolute state per tab and supplies it to new frames", async () => {
assert.deepEqual(
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 0 }),
{ enabled: false }
{ enabled: false, fontStyle: "round" }
);
assert.deepEqual(
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: true }),
{ enabled: true, available: true }
{ enabled: true, available: true, fontStyle: "round" }
);
assert.deepEqual(fake.broadcasts.at(-1), {
tabId: 17,
message: { type: "glagolify:set-enabled", enabled: true }
message: { type: "glagolify:set-enabled", enabled: true, fontStyle: "round" }
});
assert.deepEqual(
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 4 }),
{ enabled: true }
{ enabled: true, fontStyle: "round" }
);
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: true });
@@ -69,11 +84,11 @@ test("owns absolute state per tab and supplies it to new frames", async () => {
assert.deepEqual(
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: false }),
{ enabled: false, available: true }
{ enabled: false, available: true, fontStyle: "round" }
);
assert.deepEqual(
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 5 }),
{ enabled: false }
{ enabled: false, fontStyle: "round" }
);
fake.remove(17);
@@ -87,7 +102,7 @@ test("keeps states isolated between tabs and reports protected tabs", async () =
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 }
{ enabled: false, fontStyle: "round" }
);
fake.api.tabs.sendMessage = async () => {
@@ -95,6 +110,46 @@ test("keeps states isolated between tabs and reports protected tabs", async () =
};
assert.deepEqual(
await fake.send({ type: "glagolify:get-tab-state", tabId: 99 }),
{ enabled: false, available: false }
{ enabled: false, available: false, fontStyle: "round" }
);
});
test("persists the font style and updates only enabled tabs", async () => {
const fake = makeBrowser({ glagolifyFontStyle: "angular" });
const controller = createBackgroundController(fake.api);
assert.deepEqual(
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 7 } }),
{ enabled: false, fontStyle: "angular" }
);
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 7, enabled: true });
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 8, enabled: false });
const broadcastCount = fake.broadcasts.length;
assert.deepEqual(
await fake.send({ type: "glagolify:set-font-style", fontStyle: "round" }),
{ fontStyle: "round", accepted: true }
);
assert.deepEqual(fake.storageWrites.at(-1), { glagolifyFontStyle: "round" });
assert.equal(controller.getFontStyle(), "round");
assert.deepEqual(fake.broadcasts.slice(broadcastCount), [
{
tabId: 7,
message: {
type: "glagolify:set-enabled",
enabled: true,
fontStyle: "round"
}
}
]);
const writesBeforeRejection = fake.storageWrites.length;
const broadcastsBeforeRejection = fake.broadcasts.length;
assert.deepEqual(
await fake.send({ type: "glagolify:set-font-style", fontStyle: "italic" }),
{ fontStyle: "round", accepted: false }
);
assert.equal(fake.storageWrites.length, writesBeforeRejection);
assert.equal(fake.broadcasts.length, broadcastsBeforeRejection);
});
+5 -1
View File
@@ -6,12 +6,16 @@ const { characterMap, transliterate } = require("../transliterate.js");
test("maps every modern Russian uppercase and lowercase letter", () => {
const russian = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
const expected = "ⰀⰁⰂⰃⰄⰅⰦⰆⰈⰉⰋⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰜⰝⰞⰛⰟⰠⰅⰣⰰⰱⰲⰳⰴⰵⱖⰶⰸⰹⰻⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱌⱍⱎⱋⱏⱐⰵⱓ";
const expected = "ⰀⰁⰂⰃⰄⰅⰦⰆⰈⰉⰋⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰜⰝⰞⰛⰟⰟⰉⰠⰅⰣⰰⰱⰲⰳⰴⰵⱖⰶⰸⰹⰻⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱌⱍⱎⱋⱏⱏⰹⱐⰵⱓ";
assert.equal(characterMap.size, 66);
assert.equal(transliterate(russian), expected);
});
test("uses the configured yer, yeri, yestu, and yati conventions", () => {
assert.equal(transliterate("ЬЪЫЕЭЯ ьъыеэя"), "ⰠⰟⰟⰉⰅⰅⰡ ⱐⱏⱏⰹⰵⰵⱑ");
});
test("preserves case, punctuation, spacing, digits, and non-Russian text", () => {
assert.equal(
transliterate("Привет, мир! Ёж № 5 — JavaScript."),
+3 -3
View File
@@ -30,11 +30,11 @@
["Ш", "Ⱎ"], ["ш", "ⱎ"],
["Щ", "Ⱋ"], ["щ", "ⱋ"],
["Ъ", "Ⱏ"], ["ъ", "ⱏ"],
["Ы", ""], ["ы", ""],
["Ь", ""], ["ь", ""],
["Ы", "ⰟⰉ"], ["ы", "ⱏⰹ"],
["Ь", ""], ["ь", ""],
["Э", "Ⰵ"], ["э", "ⰵ"],
["Ю", "Ⱓ"], ["ю", "ⱓ"],
["Я", ""], ["я", ""]
["Я", ""], ["я", ""]
];
const characterMap = new Map(pairs);