initial commit

This commit is contained in:
2026-02-09 19:04:19 -08:00
commit f92b57c7b6
531 changed files with 196294 additions and 0 deletions

199
vendor/yomitan/js/general/cache-map.js vendored Normal file
View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2020-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* @template [K=unknown]
* @template [V=unknown]
* Class which caches a map of values, keeping the most recently accessed values.
*/
export class CacheMap {
/**
* Creates a new CacheMap.
* @param {number} maxSize The maximum number of entries able to be stored in the cache.
* @param {number} [maxIdleTime=0] The maximum idle time (ms) before the cache is automatically cleared.
*/
constructor(maxSize, maxIdleTime = 0) {
if (!(
Number.isFinite(maxSize) &&
maxSize >= 0 &&
Math.floor(maxSize) === maxSize
)) {
throw new Error('Invalid maxCount');
}
if (!(
Number.isFinite(maxIdleTime) &&
maxIdleTime >= 0 &&
Math.floor(maxIdleTime) === maxIdleTime
)) {
throw new Error('Invalid maxIdleTime');
}
/** @type {number} */
this._maxSize = maxSize;
/** @type {number} */
this._maxIdleTime = maxIdleTime;
/** @type {?import('core').Timeout} */
this._idleTimeout = null;
/** @type {Map<K, import('cache-map').Node<K, V>>} */
this._map = new Map();
/** @type {import('cache-map').Node<K, V>} */
this._listFirst = this._createNode(null, null);
/** @type {import('cache-map').Node<K, V>} */
this._listLast = this._createNode(null, null);
this._resetEndNodes();
}
/**
* Returns the number of items in the cache.
* @type {number}
*/
get size() {
return this._map.size;
}
/**
* Returns the maximum number of items that can be added to the cache.
* @type {number}
*/
get maxSize() {
return this._maxSize;
}
/**
* Returns whether or not an element exists at the given key.
* @param {K} key The key of the element.
* @returns {boolean} `true` if an element with the specified key exists, `false` otherwise.
*/
has(key) {
return this._map.has(key);
}
/**
* Gets an element at the given key, if it exists. Otherwise, returns undefined.
* @param {K} key The key of the element.
* @returns {V|undefined} The existing value at the key, if any; `undefined` otherwise.
*/
get(key) {
const node = this._map.get(key);
if (typeof node === 'undefined') { return void 0; }
this._updateRecency(node);
return /** @type {V} */ (node.value);
}
/**
* Sets a value at a given key.
* @param {K} key The key of the element.
* @param {V} value The value to store in the cache.
*/
set(key, value) {
let node = this._map.get(key);
if (typeof node !== 'undefined') {
this._updateRecency(node);
node.value = value;
} else {
if (this._maxSize <= 0) { return; }
node = this._createNode(key, value);
this._addNode(node, this._listFirst);
this._map.set(key, node);
// Remove
for (let removeCount = this._map.size - this._maxSize; removeCount > 0; --removeCount) {
node = /** @type {import('cache-map').Node<K, V>} */ (this._listLast.previous);
this._removeNode(node);
this._map.delete(/** @type {K} */ (node.key));
}
}
}
/**
* Clears the cache.
*/
clear() {
this._map.clear();
this._resetEndNodes();
this.clearIdleTimeout();
}
/**
* Clears the idle timeout.
*/
clearIdleTimeout() {
if (this._idleTimeout === null) { return; }
clearTimeout(this._idleTimeout);
this._idleTimeout = null;
}
// Private
/**
* @param {import('cache-map').Node<K, V>} node
*/
_updateRecency(node) {
this._removeNode(node);
this._addNode(node, this._listFirst);
}
/**
* @param {?K} key
* @param {?V} value
* @returns {import('cache-map').Node<K, V>}
*/
_createNode(key, value) {
return {key, value, previous: null, next: null};
}
/**
* @param {import('cache-map').Node<K, V>} node
* @param {import('cache-map').Node<K, V>} previous
*/
_addNode(node, previous) {
this._resetIdleTimeout();
const next = previous.next;
node.next = next;
node.previous = previous;
previous.next = node;
/** @type {import('cache-map').Node<K, V>} */ (next).previous = node;
}
/**
* @param {import('cache-map').Node<K, V>} node
*/
_removeNode(node) {
/** @type {import('cache-map').Node<K, V>} */ (node.next).previous = node.previous;
/** @type {import('cache-map').Node<K, V>} */ (node.previous).next = node.next;
}
/**
* @returns {void}
*/
_resetEndNodes() {
this._listFirst.next = this._listLast;
this._listLast.previous = this._listFirst;
}
/**
* @returns {void}
*/
_resetIdleTimeout() {
if (this._maxIdleTime <= 0) { return; }
this.clearIdleTimeout();
this._idleTimeout = setTimeout(() => this.clear(), this._maxIdleTime);
}
}

View File

@@ -0,0 +1,355 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2016-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Class used to get and mutate generic properties of an object by using path strings.
*/
export class ObjectPropertyAccessor {
/**
* Create a new accessor for a specific object.
* @param {unknown} target The object which the getter and mutation methods are applied to.
*/
constructor(target) {
/** @type {unknown} */
this._target = target;
}
/**
* Gets the value at the specified path.
* @param {(string|number)[]} pathArray The path to the property on the target object.
* @param {number} [pathLength] How many parts of the pathArray to use.
* This parameter is optional and defaults to the length of pathArray.
* @returns {unknown} The value found at the path.
* @throws {Error} An error is thrown if pathArray is not valid for the target object.
*/
get(pathArray, pathLength) {
let target = this._target;
const ii = typeof pathLength === 'number' ? Math.min(pathArray.length, pathLength) : pathArray.length;
for (let i = 0; i < ii; ++i) {
const key = pathArray[i];
if (!ObjectPropertyAccessor.hasProperty(target, key)) {
throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray.slice(0, i + 1))}`);
}
target = /** @type {import('core').SerializableObject} */ (target)[key];
}
return target;
}
/**
* Sets the value at the specified path.
* @param {(string|number)[]} pathArray The path to the property on the target object.
* @param {unknown} value The value to assign to the property.
* @throws {Error} An error is thrown if pathArray is not valid for the target object.
*/
set(pathArray, value) {
const ii = pathArray.length - 1;
if (ii < 0) { throw new Error('Invalid path'); }
const target = this.get(pathArray, ii);
const key = pathArray[ii];
if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
}
/** @type {import('core').SerializableObject} */ (target)[key] = value;
}
/**
* Deletes the property of the target object at the specified path.
* @param {(string|number)[]}pathArray The path to the property on the target object.
* @throws {Error} An error is thrown if pathArray is not valid for the target object.
*/
delete(pathArray) {
const ii = pathArray.length - 1;
if (ii < 0) { throw new Error('Invalid path'); }
const target = this.get(pathArray, ii);
const key = pathArray[ii];
if (!ObjectPropertyAccessor.isValidPropertyType(target, key)) {
throw new Error(`Invalid path: ${ObjectPropertyAccessor.getPathString(pathArray)}`);
}
if (Array.isArray(target)) {
throw new Error('Invalid type');
}
delete /** @type {import('core').SerializableObject} */ (target)[key];
}
/**
* Swaps two properties of an object or array.
* @param {(string|number)[]} pathArray1 The path to the first property on the target object.
* @param {(string|number)[]} pathArray2 The path to the second property on the target object.
* @throws An error is thrown if pathArray1 or pathArray2 is not valid for the target object,
* or if the swap cannot be performed.
*/
swap(pathArray1, pathArray2) {
const ii1 = pathArray1.length - 1;
if (ii1 < 0) { throw new Error('Invalid path 1'); }
const target1 = this.get(pathArray1, ii1);
const key1 = pathArray1[ii1];
if (!ObjectPropertyAccessor.isValidPropertyType(target1, key1)) { throw new Error(`Invalid path 1: ${ObjectPropertyAccessor.getPathString(pathArray1)}`); }
const ii2 = pathArray2.length - 1;
if (ii2 < 0) { throw new Error('Invalid path 2'); }
const target2 = this.get(pathArray2, ii2);
const key2 = pathArray2[ii2];
if (!ObjectPropertyAccessor.isValidPropertyType(target2, key2)) { throw new Error(`Invalid path 2: ${ObjectPropertyAccessor.getPathString(pathArray2)}`); }
const value1 = /** @type {import('core').SerializableObject} */ (target1)[key1];
const value2 = /** @type {import('core').SerializableObject} */ (target2)[key2];
/** @type {import('core').SerializableObject} */ (target1)[key1] = value2;
try {
/** @type {import('core').SerializableObject} */ (target2)[key2] = value1;
} catch (error) {
// Revert
try {
/** @type {import('core').SerializableObject} */ (target1)[key1] = value1;
} catch (error2) {
// NOP
}
throw error;
}
}
/**
* Converts a path string to a path array.
* @param {(string|number)[]} pathArray The path array to convert.
* @returns {string} A string representation of `pathArray`.
* @throws {Error} An error is thrown if any item of `pathArray` is not a string or an integer.
*/
static getPathString(pathArray) {
const regexShort = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
let pathString = '';
let first = true;
for (let part of pathArray) {
switch (typeof part) {
case 'number':
if (Math.floor(part) !== part || part < 0) {
throw new Error('Invalid index');
}
part = `[${part}]`;
break;
case 'string':
if (!regexShort.test(part)) {
const escapedPart = part.replace(/["\\]/g, '\\$&');
part = `["${escapedPart}"]`;
} else {
if (!first) {
part = `.${part}`;
}
}
break;
default:
throw new Error(`Invalid type: ${typeof part}`);
}
pathString += part;
first = false;
}
return pathString;
}
/**
* Converts a path array to a path string. For the most part, the format of this string
* matches Javascript's notation for property access.
* @param {string} pathString The path string to convert.
* @returns {(string | number)[]} An array representation of `pathString`.
* @throws {Error} An error is thrown if `pathString` is malformed.
*/
static getPathArray(pathString) {
const pathArray = [];
/** @type {import('object-property-accessor').ParsePathStringState} */
let state = 'empty';
let quote = 0;
let value = '';
let escaped = false;
for (const c of pathString) {
const v = /** @type {number} */ (c.codePointAt(0));
switch (state) {
case 'empty': // Empty
case 'id-start': // Expecting identifier start
if (v === 0x5b) { // '['
if (state === 'id-start') {
throw new Error(`Unexpected character: ${c}`);
}
state = 'open-bracket';
} else if (
(v >= 0x41 && v <= 0x5a) || // ['A', 'Z']
(v >= 0x61 && v <= 0x7a) || // ['a', 'z']
v === 0x5f // '_'
) {
state = 'id';
value += c;
} else {
throw new Error(`Unexpected character: ${c}`);
}
break;
case 'id': // Identifier
if (
(v >= 0x41 && v <= 0x5a) || // ['A', 'Z']
(v >= 0x61 && v <= 0x7a) || // ['a', 'z']
(v >= 0x30 && v <= 0x39) || // ['0', '9']
v === 0x5f // '_'
) {
value += c;
} else {
switch (v) {
case 0x5b: // '['
pathArray.push(value);
value = '';
state = 'open-bracket';
break;
case 0x2e: // '.'
pathArray.push(value);
value = '';
state = 'id-start';
break;
default:
throw new Error(`Unexpected character: ${c}`);
}
}
break;
case 'open-bracket': // Open bracket
if (v === 0x22 || v === 0x27) { // '"' or '\''
quote = v;
state = 'string';
} else if (v >= 0x30 && v <= 0x39) { // ['0', '9']
state = 'number';
value += c;
} else {
throw new Error(`Unexpected character: ${c}`);
}
break;
case 'string': // Quoted string
if (escaped) {
value += c;
escaped = false;
} else if (v === 0x5c) { // '\\'
escaped = true;
} else if (v !== quote) {
value += c;
} else {
state = 'close-bracket';
}
break;
case 'number': // Number
if (v >= 0x30 && v <= 0x39) { // ['0', '9']
value += c;
} else if (v === 0x5d) { // ']'
pathArray.push(Number.parseInt(value, 10));
value = '';
state = 'next';
} else {
throw new Error(`Unexpected character: ${c}`);
}
break;
case 'close-bracket': // Expecting closing bracket after quoted string
if (v === 0x5d) { // ']'
pathArray.push(value);
value = '';
state = 'next';
} else {
throw new Error(`Unexpected character: ${c}`);
}
break;
case 'next': { // Expecting . or [
switch (v) {
case 0x5b: // '['
state = 'open-bracket';
break;
case 0x2e: // '.'
state = 'id-start';
break;
default:
throw new Error(`Unexpected character: ${c}`);
}
break;
}
}
}
switch (state) {
case 'empty':
case 'next':
break;
case 'id':
pathArray.push(value);
value = '';
break;
default:
throw new Error('Path not terminated correctly');
}
return pathArray;
}
/**
* Checks whether an object or array has the specified property.
* @param {unknown} object The object to test.
* @param {string|number} property The property to check for existence.
* This value should be a string if the object is a non-array object.
* For arrays, it should be an integer.
* @returns {boolean} `true` if the property exists, otherwise `false`.
*/
static hasProperty(object, property) {
switch (typeof property) {
case 'string':
return (
typeof object === 'object' &&
object !== null &&
!Array.isArray(object) &&
Object.prototype.hasOwnProperty.call(object, property)
);
case 'number':
return (
Array.isArray(object) &&
property >= 0 &&
property < object.length &&
property === Math.floor(property)
);
default:
return false;
}
}
/**
* Checks whether a property is valid for the given object
* @param {unknown} object The object to test.
* @param {string|number} property The property to check for existence.
* @returns {boolean} `true` if the property is correct for the given object type, otherwise `false`.
* For arrays, this means that the property should be a positive integer.
* For non-array objects, the property should be a string.
*/
static isValidPropertyType(object, property) {
switch (typeof property) {
case 'string':
return (
typeof object === 'object' &&
object !== null &&
!Array.isArray(object)
);
case 'number':
return (
Array.isArray(object) &&
property >= 0 &&
property === Math.floor(property)
);
default:
return false;
}
}
}

90
vendor/yomitan/js/general/regex-util.js vendored Normal file
View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2021-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/** @type {RegExp} @readonly */
const matchReplacementPattern = /\$(?:\$|&|`|'|(\d\d?)|<([^>]*)>)/g;
/**
* Applies string.replace using a regular expression and replacement string as arguments.
* A source map of the changes is also maintained.
* @param {string} text A string of the text to replace.
* @param {RegExp} pattern A regular expression to use as the replacement.
* @param {string} replacement A replacement string that follows the format of the standard
* JavaScript regular expression replacement string.
* @returns {string} A new string with the pattern replacements applied and the source map updated.
*/
export function applyTextReplacement(text, pattern, replacement) {
const isGlobal = pattern.global;
if (isGlobal) { pattern.lastIndex = 0; }
for (let loop = true; loop; loop = isGlobal) {
const match = pattern.exec(text);
if (match === null) { break; }
const matchText = match[0];
const index = match.index;
const actualReplacement = applyMatchReplacement(replacement, match);
const actualReplacementLength = actualReplacement.length;
const delta = actualReplacementLength - (matchText.length > 0 ? matchText.length : -1);
text = `${text.substring(0, index)}${actualReplacement}${text.substring(index + matchText.length)}`;
pattern.lastIndex += delta;
}
return text;
}
/**
* Applies the replacement string for a given regular expression match.
* @param {string} replacement The replacement string that follows the format of the standard
* JavaScript regular expression replacement string.
* @param {RegExpMatchArray} match A match object returned from RegExp.match.
* @returns {string} A new string with the pattern replacement applied.
*/
export function applyMatchReplacement(replacement, match) {
const pattern = matchReplacementPattern;
pattern.lastIndex = 0;
/**
* @param {string} g0
* @param {string} g1
* @param {string} g2
* @returns {string}
*/
const replacer = (g0, g1, g2) => {
if (typeof g1 !== 'undefined') {
const matchIndex = Number.parseInt(g1, 10);
if (matchIndex >= 1 && matchIndex <= match.length) {
return match[matchIndex];
}
} else if (typeof g2 !== 'undefined') {
const {groups} = match;
if (typeof groups === 'object' && groups !== null && Object.prototype.hasOwnProperty.call(groups, g2)) {
return groups[g2];
}
} else {
let {index} = match;
if (typeof index !== 'number') { index = 0; }
switch (g0) {
case '$': return '$';
case '&': return match[0];
case '`': return replacement.substring(0, index);
case '\'': return replacement.substring(index + g0.length);
}
}
return g0;
};
return replacement.replace(pattern, replacer);
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2023-2025 Yomitan Authors
* Copyright (C) 2020-2022 Yomichan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {log} from '../core/log.js';
/**
* @template [K=unknown]
* @template [V=unknown]
*/
export class TaskAccumulator {
/**
* @param {(tasks: [key: ?K, task: import('task-accumulator').Task<V>][]) => Promise<void>} runTasks
*/
constructor(runTasks) {
/** @type {?Promise<void>} */
this._deferPromise = null;
/** @type {?Promise<void>} */
this._activePromise = null;
/** @type {import('task-accumulator').Task<V>[]} */
this._tasks = [];
/** @type {import('task-accumulator').Task<V>[]} */
this._tasksActive = [];
/** @type {Map<K, import('task-accumulator').Task<V>>} */
this._uniqueTasks = new Map();
/** @type {Map<K, import('task-accumulator').Task<V>>} */
this._uniqueTasksActive = new Map();
/** @type {() => Promise<void>} */
this._runTasksBind = this._runTasks.bind(this);
/** @type {() => void} */
this._tasksCompleteBind = this._tasksComplete.bind(this);
/** @type {(tasks: [key: ?K, task: import('task-accumulator').Task<V>][]) => Promise<void>} */
this._runTasksCallback = runTasks;
}
/**
* @param {?K} key
* @param {V} data
* @returns {Promise<void>}
*/
enqueue(key, data) {
if (this._deferPromise === null) {
const promise = this._activePromise !== null ? this._activePromise : Promise.resolve();
this._deferPromise = promise.then(this._runTasksBind);
}
/** @type {import('task-accumulator').Task<V>} */
const task = {data, stale: false};
if (key !== null) {
const activeTaskInfo = this._uniqueTasksActive.get(key);
if (typeof activeTaskInfo !== 'undefined') {
activeTaskInfo.stale = true;
}
this._uniqueTasks.set(key, task);
} else {
this._tasks.push(task);
}
return this._deferPromise;
}
/**
* @returns {Promise<void>}
*/
_runTasks() {
this._deferPromise = null;
// Swap
[this._tasks, this._tasksActive] = [this._tasksActive, this._tasks];
[this._uniqueTasks, this._uniqueTasksActive] = [this._uniqueTasksActive, this._uniqueTasks];
const promise = this._runTasksAsync();
this._activePromise = promise.then(this._tasksCompleteBind);
return this._activePromise;
}
/**
* @returns {Promise<void>}
*/
async _runTasksAsync() {
try {
/** @type {[key: ?K, task: import('task-accumulator').Task<V>][]} */
const allTasks = [];
for (const taskInfo of this._tasksActive) {
allTasks.push([null, taskInfo]);
}
for (const [key, taskInfo] of this._uniqueTasksActive) {
allTasks.push([key, taskInfo]);
}
await this._runTasksCallback(allTasks);
} catch (e) {
log.error(e);
}
}
/**
* @returns {void}
*/
_tasksComplete() {
this._tasksActive.length = 0;
this._uniqueTasksActive.clear();
this._activePromise = null;
}
}