add angular-round selector
This commit is contained in:
@@ -16,9 +16,10 @@ Temporary add-ons remain installed until Firefox restarts. For permanent local u
|
|||||||
|
|
||||||
1. Open a normal web page.
|
1. Open a normal web page.
|
||||||
2. Select the Glagolify toolbar icon.
|
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.
|
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 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 (`Ⱐ` / `ⱐ`)
|
||||||
- `Ы` → Yeri (`Ⱐ` / `ⱐ`)
|
- `Ъ` → Yeru (`Ⱏ` / `ⱏ`)
|
||||||
- `Э` → Yestu, the same output as `Е` (`Ⰵ` / `ⰵ`)
|
- `Ы` → 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
|
## Automated checks
|
||||||
|
|
||||||
@@ -48,4 +54,4 @@ No install or build step is required. With Node.js available, run:
|
|||||||
npm test
|
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.
|
||||||
|
|||||||
+60
-11
@@ -1,18 +1,33 @@
|
|||||||
(function (root) {
|
(function (root) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const FONT_STYLE_KEY = "glagolifyFontStyle";
|
||||||
|
const FONT_STYLES = new Set(["round", "angular"]);
|
||||||
|
const DEFAULT_FONT_STYLE = "round";
|
||||||
|
|
||||||
function createBackgroundController(api) {
|
function createBackgroundController(api) {
|
||||||
const tabStates = new Map();
|
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) {
|
function validTabId(tabId) {
|
||||||
return Number.isInteger(tabId) && tabId >= 0;
|
return Number.isInteger(tabId) && tabId >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function broadcast(tabId, enabled) {
|
async function broadcast(tabId, enabled) {
|
||||||
|
await fontStyleReady;
|
||||||
try {
|
try {
|
||||||
await api.tabs.sendMessage(tabId, {
|
await api.tabs.sendMessage(tabId, {
|
||||||
type: "glagolify:set-enabled",
|
type: "glagolify:set-enabled",
|
||||||
enabled
|
enabled,
|
||||||
|
fontStyle
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -27,32 +42,61 @@
|
|||||||
|
|
||||||
if (message.type === "glagolify:document-ready") {
|
if (message.type === "glagolify:document-ready") {
|
||||||
const tabId = sender && sender.tab && sender.tab.id;
|
const tabId = sender && sender.tab && sender.tab.id;
|
||||||
return Promise.resolve({
|
return fontStyleReady.then(() => ({
|
||||||
enabled: validTabId(tabId) && tabStates.get(tabId) === true
|
enabled: validTabId(tabId) && tabStates.get(tabId) === true,
|
||||||
});
|
fontStyle
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "glagolify:get-tab-state") {
|
if (message.type === "glagolify:get-tab-state") {
|
||||||
const tabId = message.tabId;
|
const tabId = message.tabId;
|
||||||
|
return fontStyleReady.then(async () => {
|
||||||
if (!validTabId(tabId)) {
|
if (!validTabId(tabId)) {
|
||||||
return Promise.resolve({ enabled: false, available: false });
|
return { enabled: false, available: false, fontStyle };
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = tabStates.get(tabId) === true;
|
const enabled = tabStates.get(tabId) === true;
|
||||||
return broadcast(tabId, enabled).then((available) => ({ enabled, available }));
|
const available = await broadcast(tabId, enabled);
|
||||||
|
return { enabled, available, fontStyle };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "glagolify:set-tab-enabled") {
|
if (message.type === "glagolify:set-tab-enabled") {
|
||||||
const tabId = message.tabId;
|
const tabId = message.tabId;
|
||||||
if (!validTabId(tabId) || typeof message.enabled !== "boolean") {
|
if (!validTabId(tabId) || typeof message.enabled !== "boolean") {
|
||||||
return Promise.resolve({ enabled: false, available: false });
|
return fontStyleReady.then(() => ({
|
||||||
|
enabled: false,
|
||||||
|
available: false,
|
||||||
|
fontStyle
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fontStyleReady.then(async () => {
|
||||||
tabStates.set(tabId, message.enabled);
|
tabStates.set(tabId, message.enabled);
|
||||||
return broadcast(tabId, message.enabled).then((available) => ({
|
const available = await broadcast(tabId, message.enabled);
|
||||||
|
return {
|
||||||
enabled: message.enabled,
|
enabled: message.enabled,
|
||||||
available
|
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;
|
return undefined;
|
||||||
@@ -62,7 +106,12 @@
|
|||||||
tabStates.delete(tabId);
|
tabStates.delete(tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { tabStates, broadcast };
|
return {
|
||||||
|
tabStates,
|
||||||
|
broadcast,
|
||||||
|
fontStyleReady,
|
||||||
|
getFontStyle: () => fontStyle
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exported = Object.freeze({ createBackgroundController });
|
const exported = Object.freeze({ createBackgroundController });
|
||||||
|
|||||||
+10
@@ -6,3 +6,13 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
unicode-range: U+2C00-2C5F;
|
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
@@ -2,7 +2,11 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
|
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 trackedText = new Map();
|
||||||
const styledElements = new Map();
|
const styledElements = new Map();
|
||||||
const observers = new Map();
|
const observers = new Map();
|
||||||
@@ -53,8 +57,19 @@
|
|||||||
|
|
||||||
function appliedFontFamily(computedFamily) {
|
function appliedFontFamily(computedFamily) {
|
||||||
return computedFamily
|
return computedFamily
|
||||||
? `${FONT_FAMILY}, ${computedFamily}`
|
? `${fontFamily}, ${computedFamily}`
|
||||||
: `${FONT_FAMILY}, sans-serif`;
|
: `${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) {
|
function writeAppliedFont(element, record, computedFamily) {
|
||||||
@@ -77,12 +92,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const computedFamily = getComputedStyle(element).fontFamily;
|
||||||
const record = {
|
const record = {
|
||||||
pageValue: element.style.getPropertyValue("font-family"),
|
pageValue: element.style.getPropertyValue("font-family"),
|
||||||
pagePriority: element.style.getPropertyPriority("font-family"),
|
pagePriority: element.style.getPropertyPriority("font-family"),
|
||||||
|
pageComputedFamily: computedFamily,
|
||||||
appliedValue: ""
|
appliedValue: ""
|
||||||
};
|
};
|
||||||
const computedFamily = getComputedStyle(element).fontFamily;
|
|
||||||
styledElements.set(element, record);
|
styledElements.set(element, record);
|
||||||
writeAppliedFont(element, record, computedFamily);
|
writeAppliedFont(element, record, computedFamily);
|
||||||
}
|
}
|
||||||
@@ -101,7 +117,8 @@
|
|||||||
|
|
||||||
record.pageValue = currentValue;
|
record.pageValue = currentValue;
|
||||||
record.pagePriority = currentPriority;
|
record.pagePriority = currentPriority;
|
||||||
writeAppliedFont(element, record, getComputedStyle(element).fontFamily);
|
record.pageComputedFamily = getComputedStyle(element).fontFamily;
|
||||||
|
writeAppliedFont(element, record, record.pageComputedFamily);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreFont(element, record) {
|
function restoreFont(element, record) {
|
||||||
@@ -301,7 +318,8 @@
|
|||||||
ownStyleMutationCounts = new WeakMap();
|
ownStyleMutationCounts = new WeakMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEnabled(target) {
|
function setEnabled(target, style) {
|
||||||
|
setFontStyle(style);
|
||||||
if (target) {
|
if (target) {
|
||||||
enable();
|
enable();
|
||||||
} else {
|
} else {
|
||||||
@@ -314,12 +332,12 @@
|
|||||||
if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") {
|
if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return Promise.resolve(setEnabled(message.enabled));
|
return Promise.resolve(setEnabled(message.enabled, message.fontStyle));
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.runtime.sendMessage({ type: "glagolify:document-ready" })
|
browser.runtime.sendMessage({ type: "glagolify:document-ready" })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setEnabled(Boolean(response && response.enabled));
|
setEnabled(Boolean(response && response.enabled), response && response.fontStyle);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
|
|||||||
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Glagolify",
|
"name": "Glagolify",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Toggle modern Russian page text between Cyrillic and Glagolitic.",
|
"description": "Toggle modern Russian page text between Cyrillic and Glagolitic.",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
|
"storage",
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"fonts/NotoSansGlagolitic-Regular.ttf"
|
"fonts/NotoSansGlagolitic-Regular.ttf",
|
||||||
|
"fonts/Shafarik-Regular.ttf"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "glagolify-firefox-extension",
|
"name": "glagolify-firefox-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A dependency-free Firefox extension for toggling modern Russian text to Glagolitic.",
|
"description": "A dependency-free Firefox extension for toggling modern Russian text to Glagolitic.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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 {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
@@ -47,6 +58,80 @@ header p {
|
|||||||
font-size: 12px;
|
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 {
|
.status {
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
@@ -107,6 +192,22 @@ button:disabled {
|
|||||||
color: #bdb5c7;
|
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 {
|
button:disabled {
|
||||||
color: #77717e;
|
color: #77717e;
|
||||||
background: #343039;
|
background: #343039;
|
||||||
|
|||||||
+14
@@ -16,6 +16,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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>
|
<p id="status" class="status" aria-live="polite">Checking this page…</p>
|
||||||
<button id="toggle" type="button" aria-pressed="false" disabled>Enable Glagolitic</button>
|
<button id="toggle" type="button" aria-pressed="false" disabled>Enable Glagolitic</button>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
const status = document.getElementById("status");
|
const status = document.getElementById("status");
|
||||||
const toggle = document.getElementById("toggle");
|
const toggle = document.getElementById("toggle");
|
||||||
|
const fontStyleInputs = [...document.querySelectorAll('input[name="font-style"]')];
|
||||||
let activeTabId = null;
|
let activeTabId = null;
|
||||||
let currentEnabled = false;
|
let currentEnabled = false;
|
||||||
|
let currentFontStyle = "round";
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
let fontStyleBusy = false;
|
||||||
|
|
||||||
function render(enabled) {
|
function render(enabled) {
|
||||||
currentEnabled = enabled;
|
currentEnabled = enabled;
|
||||||
@@ -15,6 +18,20 @@
|
|||||||
status.textContent = enabled ? "Glagolitic is on for this tab." : "Cyrillic is unchanged in this tab.";
|
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() {
|
function showUnavailable() {
|
||||||
activeTabId = null;
|
activeTabId = null;
|
||||||
currentEnabled = false;
|
currentEnabled = false;
|
||||||
@@ -47,6 +64,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
render(response.enabled === true);
|
render(response.enabled === true);
|
||||||
|
renderFontStyle(response.fontStyle);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showUnavailable();
|
showUnavailable();
|
||||||
} finally {
|
} 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() {
|
async function initialize() {
|
||||||
try {
|
try {
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
@@ -69,6 +113,7 @@
|
|||||||
const response = await askBackground("glagolify:get-tab-state", {
|
const response = await askBackground("glagolify:get-tab-state", {
|
||||||
tabId: activeTabId
|
tabId: activeTabId
|
||||||
});
|
});
|
||||||
|
renderFontStyle(response && response.fontStyle);
|
||||||
if (!response || !response.available) {
|
if (!response || !response.available) {
|
||||||
showUnavailable();
|
showUnavailable();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ const test = require("node:test");
|
|||||||
const assert = require("node:assert/strict");
|
const assert = require("node:assert/strict");
|
||||||
const { createBackgroundController } = require("../background.js");
|
const { createBackgroundController } = require("../background.js");
|
||||||
|
|
||||||
function makeBrowser() {
|
function makeBrowser(initialStorage = {}) {
|
||||||
let messageListener;
|
let messageListener;
|
||||||
let removedListener;
|
let removedListener;
|
||||||
const broadcasts = [];
|
const broadcasts = [];
|
||||||
|
const stored = { ...initialStorage };
|
||||||
|
const storageWrites = [];
|
||||||
const api = {
|
const api = {
|
||||||
runtime: {
|
runtime: {
|
||||||
onMessage: {
|
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: {
|
tabs: {
|
||||||
async sendMessage(tabId, message) {
|
async sendMessage(tabId, message) {
|
||||||
broadcasts.push({ tabId, message });
|
broadcasts.push({ tabId, message });
|
||||||
@@ -32,6 +45,8 @@ function makeBrowser() {
|
|||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
broadcasts,
|
broadcasts,
|
||||||
|
stored,
|
||||||
|
storageWrites,
|
||||||
send(message, sender = {}) {
|
send(message, sender = {}) {
|
||||||
return messageListener(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(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 0 }),
|
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 0 }),
|
||||||
{ enabled: false }
|
{ enabled: false, fontStyle: "round" }
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: true }),
|
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), {
|
assert.deepEqual(fake.broadcasts.at(-1), {
|
||||||
tabId: 17,
|
tabId: 17,
|
||||||
message: { type: "glagolify:set-enabled", enabled: true }
|
message: { type: "glagolify:set-enabled", enabled: true, fontStyle: "round" }
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 4 }),
|
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 });
|
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(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: false }),
|
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 17, enabled: false }),
|
||||||
{ enabled: false, available: true }
|
{ enabled: false, available: true, fontStyle: "round" }
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 5 }),
|
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 17 }, frameId: 5 }),
|
||||||
{ enabled: false }
|
{ enabled: false, fontStyle: "round" }
|
||||||
);
|
);
|
||||||
|
|
||||||
fake.remove(17);
|
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 });
|
await fake.send({ type: "glagolify:set-tab-enabled", tabId: 3, enabled: true });
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 4 } }),
|
await fake.send({ type: "glagolify:document-ready" }, { tab: { id: 4 } }),
|
||||||
{ enabled: false }
|
{ enabled: false, fontStyle: "round" }
|
||||||
);
|
);
|
||||||
|
|
||||||
fake.api.tabs.sendMessage = async () => {
|
fake.api.tabs.sendMessage = async () => {
|
||||||
@@ -95,6 +110,46 @@ test("keeps states isolated between tabs and reports protected tabs", async () =
|
|||||||
};
|
};
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await fake.send({ type: "glagolify:get-tab-state", tabId: 99 }),
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ const { characterMap, transliterate } = require("../transliterate.js");
|
|||||||
|
|
||||||
test("maps every modern Russian uppercase and lowercase letter", () => {
|
test("maps every modern Russian uppercase and lowercase letter", () => {
|
||||||
const russian = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
|
const russian = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
|
||||||
const expected = "ⰀⰁⰂⰃⰄⰅⰦⰆⰈⰉⰋⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰜⰝⰞⰛⰟⰠⰡⰅⰣⰤⰰⰱⰲⰳⰴⰵⱖⰶⰸⰹⰻⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱌⱍⱎⱋⱏⱐⱑⰵⱓⱔ";
|
const expected = "ⰀⰁⰂⰃⰄⰅⰦⰆⰈⰉⰋⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰜⰝⰞⰛⰟⰟⰉⰠⰅⰣⰡⰰⰱⰲⰳⰴⰵⱖⰶⰸⰹⰻⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱌⱍⱎⱋⱏⱏⰹⱐⰵⱓⱑ";
|
||||||
|
|
||||||
assert.equal(characterMap.size, 66);
|
assert.equal(characterMap.size, 66);
|
||||||
assert.equal(transliterate(russian), expected);
|
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", () => {
|
test("preserves case, punctuation, spacing, digits, and non-Russian text", () => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
transliterate("Привет, мир! Ёж № 5 — JavaScript."),
|
transliterate("Привет, мир! Ёж № 5 — JavaScript."),
|
||||||
|
|||||||
+3
-3
@@ -30,11 +30,11 @@
|
|||||||
["Ш", "Ⱎ"], ["ш", "ⱎ"],
|
["Ш", "Ⱎ"], ["ш", "ⱎ"],
|
||||||
["Щ", "Ⱋ"], ["щ", "ⱋ"],
|
["Щ", "Ⱋ"], ["щ", "ⱋ"],
|
||||||
["Ъ", "Ⱏ"], ["ъ", "ⱏ"],
|
["Ъ", "Ⱏ"], ["ъ", "ⱏ"],
|
||||||
["Ы", "Ⱐ"], ["ы", "ⱐ"],
|
["Ы", "ⰟⰉ"], ["ы", "ⱏⰹ"],
|
||||||
["Ь", "Ⱑ"], ["ь", "ⱑ"],
|
["Ь", "Ⱐ"], ["ь", "ⱐ"],
|
||||||
["Э", "Ⰵ"], ["э", "ⰵ"],
|
["Э", "Ⰵ"], ["э", "ⰵ"],
|
||||||
["Ю", "Ⱓ"], ["ю", "ⱓ"],
|
["Ю", "Ⱓ"], ["ю", "ⱓ"],
|
||||||
["Я", "Ⱔ"], ["я", "ⱔ"]
|
["Я", "Ⱑ"], ["я", "ⱑ"]
|
||||||
];
|
];
|
||||||
|
|
||||||
const characterMap = new Map(pairs);
|
const characterMap = new Map(pairs);
|
||||||
|
|||||||
Reference in New Issue
Block a user