Files
SubMiner/vendor/hachidori/extension/dictionary-import.js
T
sudacode d9fdc7ef6d feat(dictionary): add Hachidori backend support
- Add backend selection, setup gating, Anki integration, and external host support
- Add launcher flags, documentation, packaging, and focused tests
- Open on-demand overlay modals on the first attempt
2026-09-22 00:21:19 -07:00

102 lines
3.8 KiB
JavaScript

/*
* Local dictionary import identity and revision helpers.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export const MAX_REVISION_COMPONENTS = 32;
export const MAX_REVISION_COMPONENT_LENGTH = 64;
export const MAX_REVISION_LENGTH = 2048;
function optionalExactString(value) {
return typeof value === "string" && value !== "" ? value : null;
}
function comparableRevision(value) {
if (typeof value !== "string"
|| value.length === 0
|| value.length > MAX_REVISION_LENGTH
|| !/^[0-9]+(?:\.[0-9]+)*$/u.test(value)) {
return null;
}
const components = value.split(".");
if (components.length > MAX_REVISION_COMPONENTS
|| components.some(component => component.length > MAX_REVISION_COMPONENT_LENGTH)) {
return null;
}
const normalised = components.map((component) => {
const withoutLeadingZeroes = component.replace(/^0+(?=[0-9])/u, "");
return withoutLeadingZeroes === "" ? "0" : withoutLeadingZeroes;
});
while (normalised.length > 1 && normalised.at(-1) === "0") normalised.pop();
return normalised;
}
function compareNumericComponent(left, right) {
if (left.length !== right.length) return left.length < right.length ? -1 : 1;
if (left === right) return 0;
return left < right ? -1 : 1;
}
export function compareDictionaryRevisions(imported, installed) {
const left = comparableRevision(imported);
const right = comparableRevision(installed);
if (left === null || right === null) return "uncomparable";
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1) {
const comparison = compareNumericComponent(left[index] ?? "0", right[index] ?? "0");
if (comparison < 0) return "lower";
if (comparison > 0) return "higher";
}
return "same";
}
export function dictionaryArchiveIdentity(index) {
if (!index || typeof index !== "object" || Array.isArray(index)
|| typeof index.title !== "string" || index.title === "") {
throw new Error("The selected archive's index.json has no dictionary title.");
}
return {
title: index.title,
revision: typeof index.revision === "string" ? index.revision : null,
indexUrl: optionalExactString(index.indexUrl),
downloadUrl: optionalExactString(index.downloadUrl),
};
}
export function dictionaryImportTarget(dictionary) {
return {
id: typeof dictionary?.id === "string" ? dictionary.id : "",
title: typeof dictionary?.title === "string" ? dictionary.title : "",
path: typeof dictionary?.path === "string" ? dictionary.path : "",
revision: typeof dictionary?.revision === "string" ? dictionary.revision : "",
sourceId: optionalExactString(dictionary?.sourceId),
indexUrl: optionalExactString(dictionary?.indexUrl),
downloadUrl: optionalExactString(dictionary?.downloadUrl),
isUpdatable: dictionary?.isUpdatable === true,
};
}
export function dictionaryImportMatches(identity, dictionaries) {
const exact = dictionaries
.filter(dictionary => dictionary?.title === identity.title)
.map(dictionary => ({ dictionary, kind: "title" }));
if (exact.length > 0 || identity.indexUrl === null) return exact;
return dictionaries
.filter(dictionary => optionalExactString(dictionary?.indexUrl) === identity.indexUrl)
.map(dictionary => ({ dictionary, kind: "source" }));
}
export function describeRevisionComparison(imported, installed) {
switch (compareDictionaryRevisions(imported, installed)) {
case "higher":
return "The imported revision is newer than the installed revision.";
case "same":
return "The imported and installed revisions are the same.";
case "lower":
return "The imported revision is older than the installed revision. Replacing will downgrade it.";
default:
return "Hachidori cannot compare these revision values. Choose how to import the archive.";
}
}