Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df16075421 |
@@ -1,4 +1,4 @@
|
|||||||
name: Release unsigned Firefox extension
|
name: Release Firefox and Chromium extensions
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
@@ -11,7 +11,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
package-and-attach:
|
package-and-attach:
|
||||||
name: Package and attach unsigned XPI
|
name: Package and attach browser extensions
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
@@ -30,14 +30,14 @@ jobs:
|
|||||||
- name: Run automated checks
|
- name: Run automated checks
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Build unsigned release archive
|
- name: Build unsigned release archives
|
||||||
env:
|
env:
|
||||||
GITEA_RELEASE_TAG: ${{ gitea.event.release.tag_name }}
|
GITEA_RELEASE_TAG: ${{ gitea.event.release.tag_name }}
|
||||||
run: npm run package -- --version "${GITEA_RELEASE_TAG}" --output-dir dist
|
run: npm run package -- --version "${GITEA_RELEASE_TAG}" --output-dir dist
|
||||||
|
|
||||||
- name: Verify release checksum
|
- name: Verify release checksum
|
||||||
working-directory: dist
|
working-directory: dist
|
||||||
run: sha256sum --check *.xpi.sha256
|
run: sha256sum --check *.sha256
|
||||||
|
|
||||||
- name: Attach release assets
|
- name: Attach release assets
|
||||||
env:
|
env:
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
GITEA_RELEASE_ID: ${{ gitea.event.release.id }}
|
GITEA_RELEASE_ID: ${{ gitea.event.release.id }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
for asset in dist/*.xpi dist/*.xpi.sha256; do
|
for asset in dist/*.xpi dist/*.xpi.sha256 dist/*-chromium.zip dist/*-chromium.zip.sha256; do
|
||||||
name="$(basename "${asset}")"
|
name="$(basename "${asset}")"
|
||||||
curl --fail-with-body --silent --show-error \
|
curl --fail-with-body --silent --show-error \
|
||||||
--request POST \
|
--request POST \
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Glagolify
|
# Glagolify
|
||||||
|
|
||||||
Glagolify is a small Firefox extension that turns Russian Cyrillic text on webpages into Glagolitic. Switch it off and the original text comes back—no reload required.
|
Glagolify is a small Firefox and Chromium extension that turns Russian Cyrillic text on webpages into Glagolitic. Switch it off and the original text comes back—no reload required.
|
||||||
|
|
||||||
Choose from four letter styles: **Round**, **Angular Sans**, **Historical Angular**, and **Missal Angular**. Glagolify also handles text added after a page loads while leaving text fields and editors alone.
|
Choose from four letter styles: **Round**, **Angular Sans**, **Historical Angular**, and **Missal Angular**. Glagolify also handles text added after a page loads while leaving text fields and editors alone.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
### Firefox
|
||||||
|
|
||||||
Glagolify requires Firefox 142 or newer.
|
Glagolify requires Firefox 142 or newer.
|
||||||
|
|
||||||
1. Download the latest `.xpi` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases).
|
1. Download the latest `.xpi` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases).
|
||||||
@@ -15,8 +17,33 @@ Glagolify requires Firefox 142 or newer.
|
|||||||
|
|
||||||
Release builds are currently unsigned, so regular Firefox installs them temporarily and removes them when Firefox restarts.
|
Release builds are currently unsigned, so regular Firefox installs them temporarily and removes them when Firefox restarts.
|
||||||
|
|
||||||
|
### Chromium
|
||||||
|
|
||||||
|
Glagolify requires Chromium 102 or newer.
|
||||||
|
|
||||||
|
1. Download and extract the latest `-chromium.zip` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases).
|
||||||
|
2. Open `chrome://extensions` in Chromium and enable **Developer mode**.
|
||||||
|
3. Select **Load unpacked** and choose the extracted directory.
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
Open a webpage, click the Glagolify toolbar icon, choose a style, and select **Enable Glagolitic**. Select **Restore Cyrillic** when you want the original text back.
|
Open a webpage, click the Glagolify toolbar icon, choose a style, and select **Enable Glagolitic**. Select **Restore Cyrillic** when you want the original text back.
|
||||||
|
|
||||||
Glagolify transliterates letters; it does not translate language. Firefox settings pages, browser UI, and other protected pages cannot be changed.
|
Glagolify transliterates letters; it does not translate language. Browser settings pages, browser UI, and other protected pages cannot be changed.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Node.js 22 or newer is required. The build has no third-party dependencies.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm test
|
||||||
|
npm run package
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run package` creates both `dist/glagolify-<version>.xpi` for Firefox and `dist/glagolify-<version>-chromium.zip`, with a `.sha256` file beside each archive. Use `npm run package:firefox` or `npm run package:chromium` to build only one target. A release version and output directory can be supplied to any command, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run package:chromium -- --version v1.2.3 --output-dir dist
|
||||||
|
```
|
||||||
|
|
||||||
|
The checked-in manifest remains the Firefox Manifest V2 source. The Chromium package command converts it to Manifest V3 and uses the shared runtime files.
|
||||||
|
|||||||
+59
-14
@@ -2,6 +2,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const FONT_STYLE_KEY = "glagolifyFontStyle";
|
const FONT_STYLE_KEY = "glagolifyFontStyle";
|
||||||
|
const TAB_STATES_KEY = "glagolifyTabStates";
|
||||||
const FONT_STYLES = new Set(["round", "angular-sans", "angular", "missal-angular"]);
|
const FONT_STYLES = new Set(["round", "angular-sans", "angular", "missal-angular"]);
|
||||||
const DEFAULT_FONT_STYLE = "round";
|
const DEFAULT_FONT_STYLE = "round";
|
||||||
|
|
||||||
@@ -13,14 +14,35 @@
|
|||||||
if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) {
|
if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) {
|
||||||
fontStyle = stored[FONT_STYLE_KEY];
|
fontStyle = stored[FONT_STYLE_KEY];
|
||||||
}
|
}
|
||||||
return fontStyle;
|
|
||||||
})
|
})
|
||||||
.catch(() => fontStyle);
|
.catch(() => undefined);
|
||||||
|
const tabStatesReady = Promise.resolve(api.storage.session.get(TAB_STATES_KEY))
|
||||||
|
.then((stored) => {
|
||||||
|
const states = stored && stored[TAB_STATES_KEY];
|
||||||
|
if (!states || typeof states !== "object" || Array.isArray(states)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [storedTabId, enabled] of Object.entries(states)) {
|
||||||
|
const tabId = Number(storedTabId);
|
||||||
|
if (validTabId(tabId) && typeof enabled === "boolean") {
|
||||||
|
tabStates.set(tabId, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
const controllerReady = Promise.all([fontStyleReady, tabStatesReady]);
|
||||||
|
|
||||||
function validTabId(tabId) {
|
function validTabId(tabId) {
|
||||||
return Number.isInteger(tabId) && tabId >= 0;
|
return Number.isInteger(tabId) && tabId >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistTabStates() {
|
||||||
|
await api.storage.session.set({
|
||||||
|
[TAB_STATES_KEY]: Object.fromEntries(tabStates)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function broadcast(tabId, enabled) {
|
async function broadcast(tabId, enabled) {
|
||||||
await fontStyleReady;
|
await fontStyleReady;
|
||||||
try {
|
try {
|
||||||
@@ -35,14 +57,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
api.runtime.onMessage.addListener((message, sender) => {
|
function handleMessage(message, sender) {
|
||||||
if (!message || typeof message.type !== "string") {
|
if (!message || typeof message.type !== "string") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 fontStyleReady.then(() => ({
|
return controllerReady.then(() => ({
|
||||||
enabled: validTabId(tabId) && tabStates.get(tabId) === true,
|
enabled: validTabId(tabId) && tabStates.get(tabId) === true,
|
||||||
fontStyle
|
fontStyle
|
||||||
}));
|
}));
|
||||||
@@ -50,7 +72,7 @@
|
|||||||
|
|
||||||
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 () => {
|
return controllerReady.then(async () => {
|
||||||
if (!validTabId(tabId)) {
|
if (!validTabId(tabId)) {
|
||||||
return { enabled: false, available: false, fontStyle };
|
return { enabled: false, available: false, fontStyle };
|
||||||
}
|
}
|
||||||
@@ -64,15 +86,16 @@
|
|||||||
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 fontStyleReady.then(() => ({
|
return controllerReady.then(() => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
available: false,
|
available: false,
|
||||||
fontStyle
|
fontStyle
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontStyleReady.then(async () => {
|
return controllerReady.then(async () => {
|
||||||
tabStates.set(tabId, message.enabled);
|
tabStates.set(tabId, message.enabled);
|
||||||
|
await persistTabStates();
|
||||||
const available = await broadcast(tabId, message.enabled);
|
const available = await broadcast(tabId, message.enabled);
|
||||||
return {
|
return {
|
||||||
enabled: message.enabled,
|
enabled: message.enabled,
|
||||||
@@ -84,10 +107,10 @@
|
|||||||
|
|
||||||
if (message.type === "glagolify:set-font-style") {
|
if (message.type === "glagolify:set-font-style") {
|
||||||
if (!FONT_STYLES.has(message.fontStyle)) {
|
if (!FONT_STYLES.has(message.fontStyle)) {
|
||||||
return fontStyleReady.then(() => ({ fontStyle, accepted: false }));
|
return controllerReady.then(() => ({ fontStyle, accepted: false }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontStyleReady.then(async () => {
|
return controllerReady.then(async () => {
|
||||||
await api.storage.local.set({ [FONT_STYLE_KEY]: message.fontStyle });
|
await api.storage.local.set({ [FONT_STYLE_KEY]: message.fontStyle });
|
||||||
fontStyle = message.fontStyle;
|
fontStyle = message.fontStyle;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@@ -100,24 +123,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
const response = handleMessage(message, sender);
|
||||||
|
if (response === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.resolve(response).then(
|
||||||
|
(value) => sendResponse(value),
|
||||||
|
() => sendResponse(undefined)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
api.tabs.onRemoved.addListener((tabId) => {
|
api.tabs.onRemoved.addListener(async (tabId) => {
|
||||||
tabStates.delete(tabId);
|
await tabStatesReady;
|
||||||
|
if (tabStates.delete(tabId)) {
|
||||||
|
await persistTabStates();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tabStates,
|
tabStates,
|
||||||
broadcast,
|
broadcast,
|
||||||
fontStyleReady,
|
controllerReady,
|
||||||
getFontStyle: () => fontStyle
|
getFontStyle: () => fontStyle
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const exported = Object.freeze({ createBackgroundController });
|
const exported = Object.freeze({ createBackgroundController });
|
||||||
|
|
||||||
if (typeof browser === "object" && browser.runtime && browser.tabs) {
|
const extensionApi =
|
||||||
createBackgroundController(browser);
|
typeof browser === "object"
|
||||||
|
? browser
|
||||||
|
: typeof chrome === "object"
|
||||||
|
? chrome
|
||||||
|
: null;
|
||||||
|
if (extensionApi && extensionApi.runtime && extensionApi.tabs) {
|
||||||
|
createBackgroundController(extensionApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof module === "object" && module.exports) {
|
if (typeof module === "object" && module.exports) {
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const api = typeof browser === "object" ? browser : chrome;
|
||||||
|
|
||||||
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
|
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
|
||||||
const FONT_FAMILIES = Object.freeze({
|
const FONT_FAMILIES = Object.freeze({
|
||||||
round: '"Glagolify Shafarik Round"',
|
round: '"Glagolify Shafarik Round"',
|
||||||
@@ -330,14 +332,15 @@
|
|||||||
return { enabled };
|
return { enabled };
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message) => {
|
api.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
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 false;
|
||||||
}
|
}
|
||||||
return Promise.resolve(setEnabled(message.enabled, message.fontStyle));
|
sendResponse(setEnabled(message.enabled, message.fontStyle));
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.runtime.sendMessage({ type: "glagolify:document-ready" })
|
api.runtime.sendMessage({ type: "glagolify:document-ready" })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setEnabled(Boolean(response && response.enabled), response && response.fontStyle);
|
setEnabled(Boolean(response && response.enabled), response && response.fontStyle);
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 623 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "glagolify-firefox-extension",
|
"name": "glagolify-extension",
|
||||||
"version": "1.1.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 and Chromium extension for toggling modern Russian text to Glagolitic.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"package": "node scripts/package-extension.js",
|
"package": "node scripts/package-extension.js",
|
||||||
|
"package:firefox": "node scripts/package-extension.js --target firefox",
|
||||||
|
"package:chromium": "node scripts/package-extension.js --target chromium",
|
||||||
"test": "node --test"
|
"test": "node --test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const api = typeof browser === "object" ? browser : chrome;
|
||||||
|
|
||||||
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"]')];
|
const fontStyleInputs = [...document.querySelectorAll('input[name="font-style"]')];
|
||||||
@@ -39,11 +41,11 @@
|
|||||||
toggle.disabled = true;
|
toggle.disabled = true;
|
||||||
toggle.setAttribute("aria-pressed", "false");
|
toggle.setAttribute("aria-pressed", "false");
|
||||||
toggle.textContent = "Unavailable on this page";
|
toggle.textContent = "Unavailable on this page";
|
||||||
status.textContent = "Firefox protects this page from extensions.";
|
status.textContent = "The browser protects this page from extensions.";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askBackground(type, extra = {}) {
|
async function askBackground(type, extra = {}) {
|
||||||
return browser.runtime.sendMessage({ type, ...extra });
|
return api.runtime.sendMessage({ type, ...extra });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle.addEventListener("click", async () => {
|
toggle.addEventListener("click", async () => {
|
||||||
@@ -104,7 +106,7 @@
|
|||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
try {
|
try {
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabs = await api.tabs.query({ active: true, currentWindow: true });
|
||||||
if (!tabs[0] || typeof tabs[0].id !== "number") {
|
if (!tabs[0] || typeof tabs[0].id !== "number") {
|
||||||
showUnavailable();
|
showUnavailable();
|
||||||
return;
|
return;
|
||||||
|
|||||||
+103
-20
@@ -5,7 +5,7 @@ const { mkdir, readFile, writeFile } = require("node:fs/promises");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { deflateRawSync } = require("node:zlib");
|
const { deflateRawSync } = require("node:zlib");
|
||||||
|
|
||||||
const ARCHIVE_FILES = Object.freeze([
|
const FIREFOX_ARCHIVE_FILES = Object.freeze([
|
||||||
"background.js",
|
"background.js",
|
||||||
"content.css",
|
"content.css",
|
||||||
"content.js",
|
"content.js",
|
||||||
@@ -23,6 +23,14 @@ const ARCHIVE_FILES = Object.freeze([
|
|||||||
"popup.js",
|
"popup.js",
|
||||||
"transliterate.js"
|
"transliterate.js"
|
||||||
]);
|
]);
|
||||||
|
const CHROMIUM_ARCHIVE_FILES = Object.freeze([
|
||||||
|
...FIREFOX_ARCHIVE_FILES.map((relativePath) =>
|
||||||
|
relativePath.endsWith(".svg") ? relativePath.replace(/\.svg$/, ".png") : relativePath
|
||||||
|
),
|
||||||
|
"icons/icon-128.png",
|
||||||
|
"icons/icon-48.svg"
|
||||||
|
]);
|
||||||
|
const TARGETS = new Set(["firefox", "chromium"]);
|
||||||
|
|
||||||
const VERSION_PATTERN = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/;
|
const VERSION_PATTERN = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/;
|
||||||
const UTF8_FLAG = 0x0800;
|
const UTF8_FLAG = 0x0800;
|
||||||
@@ -46,6 +54,47 @@ function normalizeVersion(value) {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createManifest(contents, version, target) {
|
||||||
|
const manifest = JSON.parse(contents.toString("utf8"));
|
||||||
|
manifest.version = version;
|
||||||
|
|
||||||
|
if (target === "firefox") {
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version.split(".").some((part) => Number(part) > 65535)) {
|
||||||
|
throw new Error(`Chromium extension version parts cannot exceed 65535: ${version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.manifest_version = 3;
|
||||||
|
manifest.minimum_chrome_version = "102";
|
||||||
|
delete manifest.browser_specific_settings;
|
||||||
|
manifest.host_permissions = manifest.permissions.filter(
|
||||||
|
(permission) => permission === "<all_urls>" || permission.includes("://")
|
||||||
|
);
|
||||||
|
manifest.permissions = manifest.permissions.filter(
|
||||||
|
(permission) => !manifest.host_permissions.includes(permission)
|
||||||
|
);
|
||||||
|
manifest.background = { service_worker: "background.js" };
|
||||||
|
manifest.action = manifest.browser_action;
|
||||||
|
delete manifest.browser_action;
|
||||||
|
manifest.web_accessible_resources = [
|
||||||
|
{
|
||||||
|
resources: manifest.web_accessible_resources,
|
||||||
|
matches: ["<all_urls>"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const icons of [manifest.icons, manifest.action.default_icon]) {
|
||||||
|
for (const size of Object.keys(icons)) {
|
||||||
|
icons[size] = icons[size].replace(/\.svg$/, ".png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manifest.icons["128"] = "icons/icon-128.png";
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
function crc32(buffer) {
|
function crc32(buffer) {
|
||||||
let crc = 0xffffffff;
|
let crc = 0xffffffff;
|
||||||
|
|
||||||
@@ -122,19 +171,29 @@ function createZip(entries) {
|
|||||||
return Buffer.concat([...localRecords, centralDirectory, endRecord]);
|
return Buffer.concat([...localRecords, centralDirectory, endRecord]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function packageExtension({ rootDirectory, outputDirectory, version: requestedVersion }) {
|
async function packageExtension({
|
||||||
|
rootDirectory,
|
||||||
|
outputDirectory,
|
||||||
|
target = "firefox",
|
||||||
|
version: requestedVersion
|
||||||
|
}) {
|
||||||
|
if (!TARGETS.has(target)) {
|
||||||
|
throw new Error(`Unknown extension target: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
const packageMetadata = JSON.parse(
|
const packageMetadata = JSON.parse(
|
||||||
await readFile(path.join(rootDirectory, "package.json"), "utf8")
|
await readFile(path.join(rootDirectory, "package.json"), "utf8")
|
||||||
);
|
);
|
||||||
const version = normalizeVersion(requestedVersion ?? packageMetadata.version);
|
const version = normalizeVersion(requestedVersion ?? packageMetadata.version);
|
||||||
|
const archiveFiles =
|
||||||
|
target === "firefox" ? FIREFOX_ARCHIVE_FILES : CHROMIUM_ARCHIVE_FILES;
|
||||||
const entries = [];
|
const entries = [];
|
||||||
|
|
||||||
for (const relativePath of ARCHIVE_FILES) {
|
for (const relativePath of archiveFiles) {
|
||||||
let contents = await readFile(path.join(rootDirectory, relativePath));
|
let contents = await readFile(path.join(rootDirectory, relativePath));
|
||||||
|
|
||||||
if (relativePath === "manifest.json") {
|
if (relativePath === "manifest.json") {
|
||||||
const manifest = JSON.parse(contents.toString("utf8"));
|
const manifest = createManifest(contents, version, target);
|
||||||
manifest.version = version;
|
|
||||||
contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +201,10 @@ async function packageExtension({ rootDirectory, outputDirectory, version: reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
const archive = createZip(entries);
|
const archive = createZip(entries);
|
||||||
const archiveName = `glagolify-${version}.xpi`;
|
const archiveName =
|
||||||
|
target === "firefox"
|
||||||
|
? `glagolify-${version}.xpi`
|
||||||
|
: `glagolify-${version}-chromium.zip`;
|
||||||
const archivePath = path.join(outputDirectory, archiveName);
|
const archivePath = path.join(outputDirectory, archiveName);
|
||||||
const checksumPath = `${archivePath}.sha256`;
|
const checksumPath = `${archivePath}.sha256`;
|
||||||
const digest = createHash("sha256").update(archive).digest("hex");
|
const digest = createHash("sha256").update(archive).digest("hex");
|
||||||
@@ -151,17 +213,22 @@ async function packageExtension({ rootDirectory, outputDirectory, version: reque
|
|||||||
await writeFile(archivePath, archive);
|
await writeFile(archivePath, archive);
|
||||||
await writeFile(checksumPath, `${digest} ${archiveName}\n`);
|
await writeFile(checksumPath, `${digest} ${archiveName}\n`);
|
||||||
|
|
||||||
return { archiveName, archivePath, checksumPath, digest, version };
|
return { archiveName, archivePath, checksumPath, digest, target, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArguments(arguments_) {
|
function parseArguments(arguments_) {
|
||||||
let outputDirectory = "dist";
|
let outputDirectory = "dist";
|
||||||
|
let target = "all";
|
||||||
let version;
|
let version;
|
||||||
|
|
||||||
for (let index = 0; index < arguments_.length; index += 1) {
|
for (let index = 0; index < arguments_.length; index += 1) {
|
||||||
const argument = arguments_[index];
|
const argument = arguments_[index];
|
||||||
|
|
||||||
if (argument === "--version" || argument === "--output-dir") {
|
if (
|
||||||
|
argument === "--version" ||
|
||||||
|
argument === "--output-dir" ||
|
||||||
|
argument === "--target"
|
||||||
|
) {
|
||||||
const value = arguments_[index + 1];
|
const value = arguments_[index + 1];
|
||||||
if (!value || value.startsWith("--")) {
|
if (!value || value.startsWith("--")) {
|
||||||
throw new Error(`${argument} requires a value.`);
|
throw new Error(`${argument} requires a value.`);
|
||||||
@@ -169,8 +236,10 @@ function parseArguments(arguments_) {
|
|||||||
|
|
||||||
if (argument === "--version") {
|
if (argument === "--version") {
|
||||||
version = value;
|
version = value;
|
||||||
} else {
|
} else if (argument === "--output-dir") {
|
||||||
outputDirectory = value;
|
outputDirectory = value;
|
||||||
|
} else {
|
||||||
|
target = value;
|
||||||
}
|
}
|
||||||
index += 1;
|
index += 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -178,22 +247,31 @@ function parseArguments(arguments_) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { outputDirectory, version };
|
if (target !== "all" && !TARGETS.has(target)) {
|
||||||
|
throw new Error(`Unknown extension target: ${target}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { outputDirectory, target, version };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const rootDirectory = path.resolve(__dirname, "..");
|
const rootDirectory = path.resolve(__dirname, "..");
|
||||||
const options = parseArguments(process.argv.slice(2));
|
const options = parseArguments(process.argv.slice(2));
|
||||||
const result = await packageExtension({
|
const targets = options.target === "all" ? [...TARGETS] : [options.target];
|
||||||
rootDirectory,
|
|
||||||
outputDirectory: path.resolve(rootDirectory, options.outputDirectory),
|
|
||||||
version: options.version
|
|
||||||
});
|
|
||||||
|
|
||||||
process.stdout.write(
|
for (const target of targets) {
|
||||||
`Created ${path.relative(rootDirectory, result.archivePath)} for version ${result.version}\n` +
|
const result = await packageExtension({
|
||||||
`SHA-256 ${result.digest}\n`
|
rootDirectory,
|
||||||
);
|
outputDirectory: path.resolve(rootDirectory, options.outputDirectory),
|
||||||
|
target,
|
||||||
|
version: options.version
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
`Created ${path.relative(rootDirectory, result.archivePath)} for ${target} ` +
|
||||||
|
`version ${result.version}\nSHA-256 ${result.digest}\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
@@ -203,4 +281,9 @@ if (require.main === module) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { ARCHIVE_FILES, normalizeVersion, packageExtension };
|
module.exports = {
|
||||||
|
CHROMIUM_ARCHIVE_FILES,
|
||||||
|
FIREFOX_ARCHIVE_FILES,
|
||||||
|
normalizeVersion,
|
||||||
|
packageExtension
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ 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(initialStorage = {}) {
|
function makeBrowser(initialStorage = {}, initialSessionStorage = {}) {
|
||||||
let messageListener;
|
let messageListener;
|
||||||
let removedListener;
|
let removedListener;
|
||||||
const broadcasts = [];
|
const broadcasts = [];
|
||||||
const stored = { ...initialStorage };
|
const stored = { ...initialStorage };
|
||||||
|
const sessionStored = { ...initialSessionStorage };
|
||||||
const storageWrites = [];
|
const storageWrites = [];
|
||||||
|
const sessionWrites = [];
|
||||||
const api = {
|
const api = {
|
||||||
runtime: {
|
runtime: {
|
||||||
onMessage: {
|
onMessage: {
|
||||||
@@ -27,6 +29,15 @@ function makeBrowser(initialStorage = {}) {
|
|||||||
storageWrites.push({ ...values });
|
storageWrites.push({ ...values });
|
||||||
Object.assign(stored, values);
|
Object.assign(stored, values);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
async get(key) {
|
||||||
|
return { [key]: sessionStored[key] };
|
||||||
|
},
|
||||||
|
async set(values) {
|
||||||
|
sessionWrites.push(structuredClone(values));
|
||||||
|
Object.assign(sessionStored, structuredClone(values));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
@@ -45,13 +56,24 @@ function makeBrowser(initialStorage = {}) {
|
|||||||
return {
|
return {
|
||||||
api,
|
api,
|
||||||
broadcasts,
|
broadcasts,
|
||||||
|
sessionStored,
|
||||||
|
sessionWrites,
|
||||||
stored,
|
stored,
|
||||||
storageWrites,
|
storageWrites,
|
||||||
send(message, sender = {}) {
|
send(message, sender = {}) {
|
||||||
return messageListener(message, sender);
|
return new Promise((resolve) => {
|
||||||
|
let responded = false;
|
||||||
|
const keepChannelOpen = messageListener(message, sender, (response) => {
|
||||||
|
responded = true;
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
if (!keepChannelOpen && !responded) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
remove(tabId) {
|
remove(tabId) {
|
||||||
removedListener(tabId);
|
return removedListener(tabId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -91,10 +113,41 @@ test("owns absolute state per tab and supplies it to new frames", async () => {
|
|||||||
{ enabled: false, fontStyle: "round" }
|
{ enabled: false, fontStyle: "round" }
|
||||||
);
|
);
|
||||||
|
|
||||||
fake.remove(17);
|
await fake.remove(17);
|
||||||
assert.equal(controller.tabStates.has(17), false);
|
assert.equal(controller.tabStates.has(17), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("restores tab state after an MV3 background worker restart", async () => {
|
||||||
|
const firstWorker = makeBrowser();
|
||||||
|
createBackgroundController(firstWorker.api);
|
||||||
|
await firstWorker.send({
|
||||||
|
type: "glagolify:set-tab-enabled",
|
||||||
|
tabId: 23,
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(firstWorker.sessionStored, {
|
||||||
|
glagolifyTabStates: { 23: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const restartedWorker = makeBrowser({}, firstWorker.sessionStored);
|
||||||
|
createBackgroundController(restartedWorker.api);
|
||||||
|
assert.deepEqual(
|
||||||
|
await restartedWorker.send(
|
||||||
|
{ type: "glagolify:document-ready" },
|
||||||
|
{ tab: { id: 23 } }
|
||||||
|
),
|
||||||
|
{ enabled: true, fontStyle: "round" }
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await restartedWorker.send({
|
||||||
|
type: "glagolify:get-tab-state",
|
||||||
|
tabId: 23
|
||||||
|
}),
|
||||||
|
{ enabled: true, available: true, fontStyle: "round" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("keeps states isolated between tabs and reports protected tabs", async () => {
|
test("keeps states isolated between tabs and reports protected tabs", async () => {
|
||||||
const fake = makeBrowser();
|
const fake = makeBrowser();
|
||||||
createBackgroundController(fake.api);
|
createBackgroundController(fake.api);
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const os = require("node:os");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { inflateRawSync } = require("node:zlib");
|
const { inflateRawSync } = require("node:zlib");
|
||||||
const {
|
const {
|
||||||
ARCHIVE_FILES,
|
CHROMIUM_ARCHIVE_FILES,
|
||||||
|
FIREFOX_ARCHIVE_FILES,
|
||||||
normalizeVersion,
|
normalizeVersion,
|
||||||
packageExtension
|
packageExtension
|
||||||
} = require("../scripts/package-extension.js");
|
} = require("../scripts/package-extension.js");
|
||||||
@@ -72,8 +73,12 @@ test("builds a deterministic unsigned XPI with the release version and runtime f
|
|||||||
const entries = readZipEntries(firstArchive);
|
const entries = readZipEntries(firstArchive);
|
||||||
|
|
||||||
assert.equal(first.archiveName, "glagolify-9.8.7.xpi");
|
assert.equal(first.archiveName, "glagolify-9.8.7.xpi");
|
||||||
assert.deepEqual([...entries.keys()], [...ARCHIVE_FILES]);
|
assert.deepEqual([...entries.keys()], [...FIREFOX_ARCHIVE_FILES]);
|
||||||
assert.equal(JSON.parse(entries.get("manifest.json")).version, "9.8.7");
|
const manifest = JSON.parse(entries.get("manifest.json"));
|
||||||
|
assert.equal(manifest.version, "9.8.7");
|
||||||
|
assert.equal(manifest.manifest_version, 2);
|
||||||
|
assert.equal(manifest.background.persistent, true);
|
||||||
|
assert.equal(manifest.browser_action.default_popup, "popup.html");
|
||||||
assert.equal(entries.has("package.json"), false);
|
assert.equal(entries.has("package.json"), false);
|
||||||
assert.equal(entries.has("tests/package-extension.test.js"), false);
|
assert.equal(entries.has("tests/package-extension.test.js"), false);
|
||||||
assert.deepEqual(firstArchive, secondArchive);
|
assert.deepEqual(firstArchive, secondArchive);
|
||||||
@@ -84,3 +89,76 @@ test("builds a deterministic unsigned XPI with the release version and runtime f
|
|||||||
`${digest} glagolify-9.8.7.xpi\n`
|
`${digest} glagolify-9.8.7.xpi\n`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("builds a Chromium MV3 ZIP with Chromium manifest fields and raster icons", async (context) => {
|
||||||
|
const rootDirectory = path.resolve(__dirname, "..");
|
||||||
|
const outputDirectory = await mkdtemp(path.join(os.tmpdir(), "glagolify-chromium-"));
|
||||||
|
context.after(() => rm(outputDirectory, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const result = await packageExtension({
|
||||||
|
rootDirectory,
|
||||||
|
outputDirectory,
|
||||||
|
target: "chromium",
|
||||||
|
version: "v4.5.6"
|
||||||
|
});
|
||||||
|
const archive = await readFile(result.archivePath);
|
||||||
|
const entries = readZipEntries(archive);
|
||||||
|
const manifest = JSON.parse(entries.get("manifest.json"));
|
||||||
|
|
||||||
|
assert.equal(result.archiveName, "glagolify-4.5.6-chromium.zip");
|
||||||
|
assert.deepEqual([...entries.keys()], [...CHROMIUM_ARCHIVE_FILES]);
|
||||||
|
assert.equal(manifest.manifest_version, 3);
|
||||||
|
assert.equal(manifest.minimum_chrome_version, "102");
|
||||||
|
assert.equal(manifest.version, "4.5.6");
|
||||||
|
assert.deepEqual(manifest.permissions, ["activeTab", "storage"]);
|
||||||
|
assert.deepEqual(manifest.host_permissions, ["<all_urls>"]);
|
||||||
|
assert.deepEqual(manifest.background, { service_worker: "background.js" });
|
||||||
|
assert.equal(manifest.action.default_popup, "popup.html");
|
||||||
|
assert.equal("browser_action" in manifest, false);
|
||||||
|
assert.equal("browser_specific_settings" in manifest, false);
|
||||||
|
assert.deepEqual(manifest.web_accessible_resources, [
|
||||||
|
{
|
||||||
|
resources: [
|
||||||
|
"fonts/NotoSansGlagolitic-Regular.ttf",
|
||||||
|
"fonts/Shafarik-Regular.ttf"
|
||||||
|
],
|
||||||
|
matches: ["<all_urls>"]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
assert.equal(manifest.icons["128"], "icons/icon-128.png");
|
||||||
|
|
||||||
|
for (const relativePath of CHROMIUM_ARCHIVE_FILES.filter((name) =>
|
||||||
|
name.endsWith(".png")
|
||||||
|
)) {
|
||||||
|
assert.deepEqual(
|
||||||
|
entries.get(relativePath).subarray(0, 8),
|
||||||
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const popup = entries.get("popup.html").toString("utf8");
|
||||||
|
for (const [, relativePath] of popup.matchAll(/\b(?:href|src)="([^"]+)"/g)) {
|
||||||
|
assert.equal(
|
||||||
|
entries.has(relativePath),
|
||||||
|
true,
|
||||||
|
`${relativePath} referenced by popup.html should be packaged`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const digest = createHash("sha256").update(archive).digest("hex");
|
||||||
|
assert.equal(
|
||||||
|
await readFile(result.checksumPath, "utf8"),
|
||||||
|
`${digest} glagolify-4.5.6-chromium.zip\n`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects Chromium version components outside the MV3 manifest range", async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
packageExtension({
|
||||||
|
rootDirectory: path.resolve(__dirname, ".."),
|
||||||
|
outputDirectory: os.tmpdir(),
|
||||||
|
target: "chromium",
|
||||||
|
version: "65536"
|
||||||
|
}),
|
||||||
|
/cannot exceed 65535/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user