This commit is contained in:
2025-09-14 23:43:10 +08:00
commit abdbd63d63
1222 changed files with 720115 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
export class AbortReason {
constructor(reason) {
this.reason = reason;
}
toString() {
return this.reason;
}
}

View File

@@ -0,0 +1,139 @@
import { saveSettingsDebounced } from '../../script.js';
const MIGRATED_MARKER = '__migrated';
const MIGRATABLE_KEYS = [
/^AlertRegex_/,
/^AlertWI_/,
/^Assets_SkipConfirm_/,
/^Characters_PerPage$/,
/^DataBank_sortField$/,
/^DataBank_sortOrder$/,
/^extension_update_nag$/,
/^extensions_sortByName$/,
/^FeatherlessModels_PerPage$/,
/^GroupMembers_PerPage$/,
/^GroupCandidates_PerPage$/,
/^LNavLockOn$/,
/^LNavOpened$/,
/^mediaWarningShown:/,
/^NavLockOn$/,
/^NavOpened$/,
/^Personas_PerPage$/,
/^Personas_GridView$/,
/^Proxy_SkipConfirm_/,
/^qr--executeShortcut$/,
/^qr--syntax$/,
/^qr--tabSize$/,
/^qr--wrap$/,
/^RegenerateWithCtrlEnter$/,
/^SelectedNavTab$/,
/^sendAsNamelessWarningShown$/,
/^StoryStringValidationCache$/,
/^WINavOpened$/,
/^WI_PerPage$/,
/^world_info_sort_order$/,
];
/**
* Provides access to account storage of arbitrary key-value pairs.
*/
class AccountStorage {
/**
* @type {Record<string, string>} Storage state
*/
#state = {};
/**
* @type {boolean} If the storage was initialized
*/
#ready = false;
#migrateLocalStorage() {
const localStorageKeys = [];
for (let i = 0; i < globalThis.localStorage.length; i++) {
localStorageKeys.push(globalThis.localStorage.key(i));
}
for (const key of localStorageKeys) {
if (MIGRATABLE_KEYS.some(k => k.test(key))) {
const value = globalThis.localStorage.getItem(key);
this.#state[key] = value;
globalThis.localStorage.removeItem(key);
}
}
}
/**
* Initialize the account storage.
* @param {Object} state Initial state
*/
init(state) {
if (state && typeof state === 'object') {
this.#state = Object.assign(this.#state, state);
}
if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
this.#migrateLocalStorage();
this.#state[MIGRATED_MARKER] = '1';
saveSettingsDebounced();
}
this.#ready = true;
}
/**
* Get the value of a key in account storage.
* @param {string} key Key to get
* @returns {string|null} Value of the key
*/
getItem(key) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to read from ${key})`);
}
return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
}
/**
* Set a key in account storage.
* @param {string} key Key to set
* @param {string} value Value to set
*/
setItem(key, value) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to write to ${key})`);
}
this.#state[key] = String(value);
saveSettingsDebounced();
}
/**
* Remove a key from account storage.
* @param {string} key Key to remove
*/
removeItem(key) {
if (!this.#ready) {
console.warn(`AccountStorage not ready (trying to remove ${key})`);
}
if (!Object.hasOwn(this.#state, key)) {
return;
}
delete this.#state[key];
saveSettingsDebounced();
}
/**
* Gets a snapshot of the storage state.
* @returns {Record<string, string>} A deep clone of the storage state
*/
getState() {
return structuredClone(this.#state);
}
}
/**
* Account storage instance.
*/
export const accountStorage = new AccountStorage();

View File

@@ -0,0 +1,56 @@
/**
* A specialized Map class that provides consistent data storage by performing deep cloning of values.
*
* @template K, V
* @extends Map<K, V>
*/
export class StructuredCloneMap extends Map {
/**
* Constructs a new StructuredCloneMap.
* @param {object} options - Options for the map
* @param {boolean} options.cloneOnGet - Whether to clone the value when getting it from the map
* @param {boolean} options.cloneOnSet - Whether to clone the value when setting it in the map
*/
constructor({ cloneOnGet, cloneOnSet } = { cloneOnGet: true, cloneOnSet: true }) {
super();
this.cloneOnGet = cloneOnGet;
this.cloneOnSet = cloneOnSet;
}
/**
* Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.
*
* The set value will always be a deep clone of the provided value to provide consistent data storage.
*
* @param {K} key - The key to set
* @param {V} value - The value to set
* @returns {this} The updated map
*/
set(key, value) {
if (!this.cloneOnSet) {
return super.set(key, value);
}
const clonedValue = structuredClone(value);
super.set(key, clonedValue);
return this;
}
/**
* Returns a specified element from the Map object.
* If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
*
* The returned value will always be a deep clone of the cached value.
*
* @param {K} key - The key to get the value for
* @returns {V | undefined} Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
*/
get(key) {
if (!this.cloneOnGet) {
return super.get(key);
}
const value = super.get(key);
return structuredClone(value);
}
}

View File

@@ -0,0 +1,30 @@
/**
* Patches showdown to unrestrictedly unhash HTML spans.
* @param {import('showdown')} showdown The showdown object to patch
*/
export function addShowdownPatch(showdown) {
showdown.subParser('unhashHTMLSpans', function (text, options, globals) {
'use strict';
text = globals.converter._dispatch('unhashHTMLSpans.before', text, options, globals);
for (var i = 0; i < globals.gHtmlSpans.length; ++i) {
var repText = globals.gHtmlSpans[i],
// limiter to prevent infinite loop (assume 10 as limit for recurse)
limit = 0;
while (/¨C(\d+)C/.test(repText)) {
var num = RegExp.$1;
repText = repText.replace('¨C' + num + 'C', globals.gHtmlSpans[num]);
if (limit === 10000) {
console.error('maximum nesting of 10000 spans reached!!!');
break;
}
++limit;
}
text = text.replace('¨C' + i + 'C', repText);
}
text = globals.converter._dispatch('unhashHTMLSpans.after', text, options, globals);
return text;
});
}