mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-23 23:02:58 +00:00
Merge pull request #85 from koloml/feature/converting-js-to-ts
Converting modules to TypeScript
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Build the map containing both real tags and their aliases.
|
||||
*
|
||||
* @param {string[]} realAndAliasedTags List combining aliases and tag names.
|
||||
* @param {string[]} realTags List of actual tag names, excluding aliases.
|
||||
*
|
||||
* @return {Map<string, string>} Map where key is a tag or alias and value is an actual tag name.
|
||||
*/
|
||||
export function buildTagsAndAliasesMap(realAndAliasedTags, realTags) {
|
||||
/** @type {Map<string, string>} */
|
||||
const tagsAndAliasesMap = new Map();
|
||||
|
||||
for (let tagName of realTags) {
|
||||
tagsAndAliasesMap.set(tagName, tagName);
|
||||
}
|
||||
|
||||
let realTagName = null;
|
||||
|
||||
for (let tagNameOrAlias of realAndAliasedTags) {
|
||||
if (tagsAndAliasesMap.has(tagNameOrAlias)) {
|
||||
realTagName = tagNameOrAlias;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!realTagName) {
|
||||
console.warn('No real tag found for the alias:', tagNameOrAlias);
|
||||
continue;
|
||||
}
|
||||
|
||||
tagsAndAliasesMap.set(tagNameOrAlias, realTagName);
|
||||
}
|
||||
|
||||
return tagsAndAliasesMap;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import PageParser from "$lib/booru/scraped/parsing/PageParser";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/TagsUtils";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils";
|
||||
|
||||
export default class PostParser extends PageParser {
|
||||
/** @type {HTMLFormElement} */
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export class Token {
|
||||
index;
|
||||
value;
|
||||
readonly index: number;
|
||||
readonly value: string;
|
||||
|
||||
constructor(index, value) {
|
||||
constructor(index: number, value: string) {
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
}
|
||||
@@ -28,12 +28,9 @@ export class BoostToken extends Token {
|
||||
}
|
||||
|
||||
export class QuotedTermToken extends Token {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
#quotedValue;
|
||||
readonly #quotedValue: string;
|
||||
|
||||
constructor(index, value, quotedValue) {
|
||||
constructor(index: number, value: string, quotedValue: string) {
|
||||
super(index, value);
|
||||
|
||||
this.#quotedValue = quotedValue;
|
||||
@@ -43,19 +40,11 @@ export class QuotedTermToken extends Token {
|
||||
return QuotedTermToken.decode(this.#quotedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {string}
|
||||
*/
|
||||
static decode(value) {
|
||||
static decode(value: string): string {
|
||||
return value.replace(/\\([\\"])/g, "$1");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {string}
|
||||
*/
|
||||
static encode(value) {
|
||||
static encode(value: string): string {
|
||||
return value.replace(/[\\"]/g, "\\$&");
|
||||
}
|
||||
}
|
||||
@@ -63,6 +52,10 @@ export class QuotedTermToken extends Token {
|
||||
export class TermToken extends Token {
|
||||
}
|
||||
|
||||
type MatchResultCarry = {
|
||||
match?: RegExpMatchArray | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Search query tokenizer. Should mostly work for the cases of parsing and finding the selected term for
|
||||
* auto-completion. Follows the rules described in the Philomena booru engine.
|
||||
@@ -70,38 +63,28 @@ export class TermToken extends Token {
|
||||
export class QueryLexer {
|
||||
/**
|
||||
* The original value to be parsed.
|
||||
* @type {string}
|
||||
*/
|
||||
#value;
|
||||
readonly #value: string;
|
||||
|
||||
/**
|
||||
* Current position of the parser in the value.
|
||||
* @type {number}
|
||||
*/
|
||||
#index = 0;
|
||||
#index: number = 0;
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
constructor(value) {
|
||||
constructor(value: string) {
|
||||
this.#value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the query and get the list of tokens.
|
||||
*
|
||||
* @return {Token[]} List of tokens.
|
||||
* @return List of tokens.
|
||||
*/
|
||||
parse() {
|
||||
/** @type {Token[]} */
|
||||
const tokens = [];
|
||||
parse(): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
const result: MatchResultCarry = {};
|
||||
|
||||
/**
|
||||
* @type {{match: RegExpMatchArray|null}}
|
||||
*/
|
||||
const result = {};
|
||||
|
||||
let dirtyText;
|
||||
let dirtyText: string;
|
||||
|
||||
while (this.#index < this.#value.length) {
|
||||
if (this.#value[this.#index] === QueryLexer.#commaCharacter) {
|
||||
@@ -111,26 +94,26 @@ export class QueryLexer {
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#negotiationOperator, result)) {
|
||||
tokens.push(new NotToken(this.#index, result.match[0]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new NotToken(this.#index, result.match![0]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#andOperator, result)) {
|
||||
tokens.push(new AndToken(this.#index, result.match[0]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new AndToken(this.#index, result.match![0]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#orOperator, result)) {
|
||||
tokens.push(new OrToken(this.#index, result.match[0]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new OrToken(this.#index, result.match![0]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#notOperator, result)) {
|
||||
tokens.push(new NotToken(this.#index, result.match[0]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new NotToken(this.#index, result.match![0]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -147,19 +130,19 @@ export class QueryLexer {
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#boostOperator, result)) {
|
||||
tokens.push(new BoostToken(this.#index, result.match[0]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new BoostToken(this.#index, result.match![0]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#whitespaces, result)) {
|
||||
this.#index += result.match[0].length;
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.#match(QueryLexer.#quotedText, result)) {
|
||||
tokens.push(new QuotedTermToken(this.#index, result.match[0], result.match[1]));
|
||||
this.#index += result.match[0].length;
|
||||
tokens.push(new QuotedTermToken(this.#index, result.match![0], result.match![1]));
|
||||
this.#index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,25 +163,25 @@ export class QueryLexer {
|
||||
/**
|
||||
* Match the provided regular expression on the string with the current parser position.
|
||||
*
|
||||
* @param {RegExp} targetRegExp Target RegExp to parse with.
|
||||
* @param {{match: any}} [resultCarrier] Object for passing the results into.
|
||||
* @param targetRegExp Target RegExp to parse with.
|
||||
* @param [resultCarrier] Object for passing the results into.
|
||||
*
|
||||
* @return {boolean} Is there a match?
|
||||
* @return Is there a match?
|
||||
*/
|
||||
#match(targetRegExp, resultCarrier = {}) {
|
||||
#match(targetRegExp: RegExp, resultCarrier: MatchResultCarry = {}): boolean {
|
||||
return this.#matchAt(targetRegExp, this.#index, resultCarrier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the provided regular expression in the string with the specific index.
|
||||
*
|
||||
* @param {RegExp} targetRegExp Target RegExp to parse with.
|
||||
* @param {number} index Index to match the expression from.
|
||||
* @param {{match: any}} [resultCarrier] Object for passing the results into.
|
||||
* @param targetRegExp Target RegExp to parse with.
|
||||
* @param index Index to match the expression from.
|
||||
* @param [resultCarrier] Object for passing the results into.
|
||||
*
|
||||
* @return {boolean} Is there a match?
|
||||
* @return Is there a match?
|
||||
*/
|
||||
#matchAt(targetRegExp, index, resultCarrier = {}) {
|
||||
#matchAt(targetRegExp: RegExp, index: number, resultCarrier: MatchResultCarry = {}): boolean {
|
||||
targetRegExp.lastIndex = index;
|
||||
resultCarrier.match = this.#value.match(targetRegExp);
|
||||
|
||||
@@ -212,11 +195,10 @@ export class QueryLexer {
|
||||
*
|
||||
* @return {string} Matched text.
|
||||
*/
|
||||
#parseDirtyText(index) {
|
||||
let resultValue = '';
|
||||
#parseDirtyText(index: number): string {
|
||||
let resultValue: string = '';
|
||||
|
||||
/** @type {{match: RegExpMatchArray|null}} */
|
||||
const result = {match: null};
|
||||
const result: MatchResultCarry = {match: null};
|
||||
|
||||
// Loop over
|
||||
while (index < this.#value.length) {
|
||||
@@ -226,8 +208,8 @@ export class QueryLexer {
|
||||
}
|
||||
|
||||
if (this.#matchAt(QueryLexer.#dirtyTextContent, index, result)) {
|
||||
resultValue += result.match[0];
|
||||
index += result.match[0].length;
|
||||
resultValue += result.match![0];
|
||||
index += result.match![0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
33
src/lib/booru/tag-utils.ts
Normal file
33
src/lib/booru/tag-utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Build the map containing both real tags and their aliases.
|
||||
*
|
||||
* @param realAndAliasedTags List combining aliases and tag names.
|
||||
* @param realTags List of actual tag names, excluding aliases.
|
||||
*
|
||||
* @return Map where key is a tag or alias and value is an actual tag name.
|
||||
*/
|
||||
export function buildTagsAndAliasesMap(realAndAliasedTags: string[], realTags: string[]): Map<string, string> {
|
||||
const tagsAndAliasesMap: Map<string, string> = new Map();
|
||||
|
||||
for (const tagName of realTags) {
|
||||
tagsAndAliasesMap.set(tagName, tagName);
|
||||
}
|
||||
|
||||
let realTagName: string | null = null;
|
||||
|
||||
for (const tagNameOrAlias of realAndAliasedTags) {
|
||||
if (tagsAndAliasesMap.has(tagNameOrAlias)) {
|
||||
realTagName = tagNameOrAlias;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!realTagName) {
|
||||
console.warn('No real tag found for the alias:', tagNameOrAlias);
|
||||
continue;
|
||||
}
|
||||
|
||||
tagsAndAliasesMap.set(tagNameOrAlias, realTagName);
|
||||
}
|
||||
|
||||
return tagsAndAliasesMap;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Helper class to read and write JSON objects to the local storage.
|
||||
* @class
|
||||
*/
|
||||
class StorageHelper {
|
||||
/**
|
||||
* @type {chrome.storage.StorageArea}
|
||||
*/
|
||||
#storageArea;
|
||||
|
||||
/**
|
||||
* @param {chrome.storage.StorageArea} storageArea
|
||||
*/
|
||||
constructor(storageArea) {
|
||||
this.#storageArea = storageArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the following entry from the local storage as a JSON object.
|
||||
*
|
||||
* @param {string} key Key of the entry to read.
|
||||
* @param {any} defaultValue Default value to return if the entry does not exist.
|
||||
*
|
||||
* @return {Promise<any>} The JSON object or the default value if the entry does not exist.
|
||||
*/
|
||||
async read(key, defaultValue = null) {
|
||||
return (await this.#storageArea.get(key))?.[key] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the following JSON object to the local storage.
|
||||
*
|
||||
* @param {string} key Key of the entry to write.
|
||||
* @param {any} value JSON object to write.
|
||||
*/
|
||||
write(key, value) {
|
||||
void this.#storageArea.set({[key]: value});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in the local storage.
|
||||
* @param {function(Record<string, chrome.storage.StorageChange>): void} callback
|
||||
*/
|
||||
subscribe(callback) {
|
||||
this.#storageArea.onChanged.addListener(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from changes in the local storage.
|
||||
* @param {function(Record<string, chrome.storage.StorageChange>): void} callback
|
||||
*/
|
||||
unsubscribe(callback) {
|
||||
this.#storageArea.onChanged.removeListener(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export default StorageHelper;
|
||||
53
src/lib/browser/StorageHelper.ts
Normal file
53
src/lib/browser/StorageHelper.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Changes subscribe function. It receives changes with old and new value for keys of the storage.
|
||||
*/
|
||||
export type StorageChangeSubscriber = (changes: Record<string, chrome.storage.StorageChange>) => void;
|
||||
|
||||
/**
|
||||
* Helper class to read and write JSON objects to the local storage.
|
||||
*/
|
||||
export default class StorageHelper {
|
||||
readonly #storageArea: chrome.storage.StorageArea;
|
||||
|
||||
constructor(storageArea: chrome.storage.StorageArea) {
|
||||
this.#storageArea = storageArea;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the following entry from the local storage as a JSON object.
|
||||
*
|
||||
* @param key Key of the entry to read.
|
||||
* @param defaultValue Default value to return if the entry does not exist.
|
||||
*
|
||||
* @return The JSON object or the default value if the entry does not exist.
|
||||
*/
|
||||
async read<Type = any, DefaultType = any>(key: string, defaultValue: DefaultType | null = null): Promise<Type | DefaultType> {
|
||||
return (await this.#storageArea.get(key))?.[key] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the following JSON object to the local storage.
|
||||
*
|
||||
* @param key Key of the entry to write.
|
||||
* @param value Value to write.
|
||||
*/
|
||||
write(key: string, value: any): void {
|
||||
void this.#storageArea.set({[key]: value});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to changes in the local storage.
|
||||
* @param callback Listener function to receive changes.
|
||||
*/
|
||||
subscribe(callback: StorageChangeSubscriber): void {
|
||||
this.#storageArea.onChanged.addListener(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from changes in the local storage.
|
||||
* @param callback Reference to the callback for unsubscribing.
|
||||
*/
|
||||
unsubscribe(callback: StorageChangeSubscriber): void {
|
||||
this.#storageArea.onChanged.removeListener(callback);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import MiscSettings from "$lib/extension/settings/MiscSettings";
|
||||
import { FullscreenViewer } from "$lib/components/FullscreenViewer";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI";
|
||||
import { tagsBlacklist } from "$config/tags";
|
||||
import { emitterAt } from "$lib/components/events/comms";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import { on } from "$lib/components/events/comms";
|
||||
import { eventMaintenanceStateChanged } from "$lib/components/events/maintenance-popup-events";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import { MaintenancePopup } from "$lib/components/MaintenancePopup";
|
||||
import { on } from "$lib/components/events/comms";
|
||||
import { eventActiveProfileChanged } from "$lib/components/events/maintenance-popup-events";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/TagsUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import { buildTagsAndAliasesMap } from "$lib/booru/tag-utils";
|
||||
import { on } from "$lib/components/events/comms";
|
||||
import { eventTagsUpdated } from "$lib/components/events/maintenance-popup-events";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver";
|
||||
|
||||
const isTagEditorProcessedKey = Symbol();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
|
||||
export class TagsForm extends BaseComponent {
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bindComponent } from "$lib/components/base/ComponentUtils";
|
||||
import { bindComponent } from "$lib/components/base/component-utils";
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
const instanceSymbol = Symbol('instance');
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element
|
||||
* @return {import('./BaseComponent').BaseComponent|null}
|
||||
*/
|
||||
export function getComponent(element) {
|
||||
return element[instanceSymbol] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the component to the selected element.
|
||||
* @param {HTMLElement} element The element to bind the component to.
|
||||
* @param {import('./BaseComponent').BaseComponent} instance The component instance.
|
||||
*/
|
||||
export function bindComponent(element, instance) {
|
||||
if (element[instanceSymbol]) {
|
||||
throw new Error('The element is already bound to a component.');
|
||||
}
|
||||
|
||||
element[instanceSymbol] = instance;
|
||||
}
|
||||
29
src/lib/components/base/component-utils.ts
Normal file
29
src/lib/components/base/component-utils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
|
||||
const instanceSymbol = Symbol('instance');
|
||||
|
||||
interface ElementWithComponent extends HTMLElement {
|
||||
[instanceSymbol]?: BaseComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component from the element, if there is one.
|
||||
* @param {HTMLElement} element
|
||||
* @return
|
||||
*/
|
||||
export function getComponent(element: ElementWithComponent): BaseComponent | null {
|
||||
return element[instanceSymbol] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the component to the selected element.
|
||||
* @param element The element to bind the component to.
|
||||
* @param instance The component instance.
|
||||
*/
|
||||
export function bindComponent(element: ElementWithComponent, instance: BaseComponent): void {
|
||||
if (element[instanceSymbol]) {
|
||||
throw new Error('The element is already bound to a component.');
|
||||
}
|
||||
|
||||
element[instanceSymbol] = instance;
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
import StorageHelper from "$lib/browser/StorageHelper";
|
||||
import StorageHelper, { type StorageChangeSubscriber } from "$lib/browser/StorageHelper";
|
||||
|
||||
export default class ConfigurationController {
|
||||
/** @type {string} */
|
||||
#configurationName;
|
||||
readonly #configurationName: string;
|
||||
|
||||
/**
|
||||
* @param {string} configurationName Name of the configuration to work with.
|
||||
*/
|
||||
constructor(configurationName) {
|
||||
constructor(configurationName: string) {
|
||||
this.#configurationName = configurationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the setting with the given name.
|
||||
*
|
||||
* @param {string} settingName Setting name.
|
||||
* @param {any} [defaultValue] Default value to return if the setting does not exist. Defaults to `null`.
|
||||
* @param settingName Setting name.
|
||||
* @param [defaultValue] Default value to return if the setting does not exist. Defaults to `null`.
|
||||
*
|
||||
* @return {Promise<any|null>} The setting value or the default value if the setting does not exist.
|
||||
* @return The setting value or the default value if the setting does not exist.
|
||||
*/
|
||||
async readSetting(settingName, defaultValue = null) {
|
||||
async readSetting<Type = any, DefaultType = any>(settingName: string, defaultValue: DefaultType | null = null): Promise<Type | DefaultType> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
return settings[settingName] ?? defaultValue;
|
||||
}
|
||||
@@ -27,12 +26,12 @@ export default class ConfigurationController {
|
||||
/**
|
||||
* Write the given value to the setting.
|
||||
*
|
||||
* @param {string} settingName Setting name.
|
||||
* @param {any} value Value to write.
|
||||
* @param settingName Setting name.
|
||||
* @param value Value to write.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async writeSetting(settingName, value) {
|
||||
async writeSetting(settingName: string, value: any): Promise<void> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
|
||||
settings[settingName] = value;
|
||||
@@ -44,10 +43,8 @@ export default class ConfigurationController {
|
||||
* Delete the specific setting.
|
||||
*
|
||||
* @param {string} settingName Setting name to delete.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async deleteSetting(settingName) {
|
||||
async deleteSetting(settingName: string): Promise<void> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
|
||||
delete settings[settingName];
|
||||
@@ -63,9 +60,8 @@ export default class ConfigurationController {
|
||||
*
|
||||
* @return {function(): void} Unsubscribe function.
|
||||
*/
|
||||
subscribeToChanges(callback) {
|
||||
/** @param {Record<string, StorageChange>} changes */
|
||||
const changesSubscriber = changes => {
|
||||
subscribeToChanges(callback: (record: Record<string, any>) => void): () => void {
|
||||
const subscriber: StorageChangeSubscriber = changes => {
|
||||
if (!changes[this.#configurationName]) {
|
||||
return;
|
||||
}
|
||||
@@ -73,9 +69,9 @@ export default class ConfigurationController {
|
||||
callback(changes[this.#configurationName].newValue);
|
||||
}
|
||||
|
||||
ConfigurationController.#storageHelper.subscribe(changesSubscriber);
|
||||
ConfigurationController.#storageHelper.subscribe(subscriber);
|
||||
|
||||
return () => ConfigurationController.#storageHelper.unsubscribe(changesSubscriber);
|
||||
return () => ConfigurationController.#storageHelper.unsubscribe(subscriber);
|
||||
}
|
||||
|
||||
static #storageHelper = new StorageHelper(chrome.storage.local);
|
||||
@@ -1,4 +1,4 @@
|
||||
import StorageHelper from "$lib/browser/StorageHelper";
|
||||
import StorageHelper, { type StorageChangeSubscriber } from "$lib/browser/StorageHelper";
|
||||
import type StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
|
||||
export default class EntitiesController {
|
||||
@@ -71,7 +71,7 @@ export default class EntitiesController {
|
||||
/**
|
||||
* Watch the changes made to the storage and call the callback when the entity changes.
|
||||
*/
|
||||
const storageChangesSubscriber = (changes: Record<string, chrome.storage.StorageChange>) => {
|
||||
const subscriber: StorageChangeSubscriber = changes => {
|
||||
if (!changes[entityName]) {
|
||||
return;
|
||||
}
|
||||
@@ -80,8 +80,8 @@ export default class EntitiesController {
|
||||
.then(callback);
|
||||
}
|
||||
|
||||
this.#storageHelper.subscribe(storageChangesSubscriber);
|
||||
this.#storageHelper.subscribe(subscriber);
|
||||
|
||||
return () => this.#storageHelper.unsubscribe(storageChangesSubscriber);
|
||||
return () => this.#storageHelper.unsubscribe(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Map of validators for each entity. Function should throw the error if validation failed.
|
||||
* @type {Map<keyof App.EntityNamesMap|string, ((importedObject: Object) => void)>}
|
||||
*/
|
||||
const entitiesValidators = new Map([
|
||||
['profiles', importedObject => {
|
||||
if (importedObject.v !== 1) {
|
||||
throw new Error('Unsupported version!');
|
||||
}
|
||||
|
||||
if (
|
||||
!importedObject.id
|
||||
|| typeof importedObject.id !== "string"
|
||||
|| !importedObject.name
|
||||
|| typeof importedObject.name !== "string"
|
||||
|| !importedObject.tags
|
||||
|| !Array.isArray(importedObject.tags)
|
||||
) {
|
||||
throw new Error('Invalid profile format detected!');
|
||||
}
|
||||
}]
|
||||
])
|
||||
|
||||
/**
|
||||
* Validate the structure of the entity.
|
||||
* @param {Object} importedObject Object imported from JSON.
|
||||
* @param {string} entityName Name of the entity to validate. Should be loaded from the entity class.
|
||||
* @throws {Error} Error in case validation failed with the reason stored in the message.
|
||||
*/
|
||||
export function validateImportedEntity(importedObject, entityName) {
|
||||
if (!entitiesValidators.has(entityName)) {
|
||||
console.error(`Trying to validate entity without the validator present! Entity name: ${entityName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
entitiesValidators
|
||||
.get(entityName)
|
||||
.call(null, importedObject);
|
||||
}
|
||||
74
src/lib/extension/transporting/validators.ts
Normal file
74
src/lib/extension/transporting/validators.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type StorageEntity from "$lib/extension/base/StorageEntity";
|
||||
|
||||
/**
|
||||
* Base information on the object which should be present on every entity.
|
||||
*/
|
||||
interface BaseImportableObject {
|
||||
/**
|
||||
* Numeric version of the entity for upgrading.
|
||||
*/
|
||||
v: number;
|
||||
/**
|
||||
* Unique ID of the entity.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility type which combines base importable object and the entity type interfaces together. It strips away any types
|
||||
* defined for the properties, since imported object can not be trusted and should be type-checked by the validators.
|
||||
*/
|
||||
type ImportableObject<EntityType extends StorageEntity> = { [ObjectKey in keyof BaseImportableObject]: any }
|
||||
& { [SettingKey in keyof EntityType["settings"]]: any };
|
||||
|
||||
/**
|
||||
* Function for validating the entities.
|
||||
* @todo Probably would be better to replace the throw-catch method with some kind of result-error returning type.
|
||||
* Errors are only properly definable in the JSDoc.
|
||||
*/
|
||||
type ValidationFunction<EntityType extends StorageEntity> = (importedObject: ImportableObject<EntityType>) => void;
|
||||
|
||||
/**
|
||||
* Mapping of validation functions for all entities present in the extension. Key is a name of entity and value is a
|
||||
* function which throws an error when validation is failed.
|
||||
*/
|
||||
type EntitiesValidationMap = {
|
||||
[EntityKey in keyof App.EntityNamesMap]?: ValidationFunction<App.EntityNamesMap[EntityKey]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of validators for each entity. Function should throw the error if validation failed.
|
||||
*/
|
||||
const entitiesValidators: EntitiesValidationMap = {
|
||||
profiles: importedObject => {
|
||||
if (importedObject.v !== 1) {
|
||||
throw new Error('Unsupported version!');
|
||||
}
|
||||
|
||||
if (
|
||||
!importedObject.id
|
||||
|| typeof importedObject.id !== "string"
|
||||
|| !importedObject.name
|
||||
|| typeof importedObject.name !== "string"
|
||||
|| !importedObject.tags
|
||||
|| !Array.isArray(importedObject.tags)
|
||||
) {
|
||||
throw new Error('Invalid profile format detected!');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the structure of the entity.
|
||||
* @param importedObject Object imported from JSON.
|
||||
* @param entityName Name of the entity to validate. Should be loaded from the entity class.
|
||||
* @throws {Error} Error in case validation failed with the reason stored in the message.
|
||||
*/
|
||||
export function validateImportedEntity(importedObject: any, entityName: string) {
|
||||
if (!entitiesValidators.hasOwnProperty(entityName)) {
|
||||
console.error(`Trying to validate entity without the validator present! Entity name: ${entityName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
entitiesValidators[entityName as keyof EntitiesValidationMap]!.call(null, importedObject);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Traverse and find the object using the key path.
|
||||
* @param {Object} targetObject Target object to traverse into.
|
||||
* @param {string[]} path Path of keys to traverse deep into the object.
|
||||
* @return {Object|null} Resulting object or null if nothing found (or target entry is not an object.
|
||||
* @param targetObject Target object to traverse into.
|
||||
* @param path Path of keys to traverse deep into the object.
|
||||
* @return Resulting object or null if nothing found (or target entry is not an object).
|
||||
*/
|
||||
export function findDeepObject(targetObject, path) {
|
||||
export function findDeepObject(targetObject: Record<string, any>, path: string[]): Object|null {
|
||||
let result = targetObject;
|
||||
|
||||
for (let key of path) {
|
||||
for (const key of path) {
|
||||
if (!result || typeof result !== 'object') {
|
||||
return null;
|
||||
}
|
||||
@@ -27,17 +27,15 @@ export function findDeepObject(targetObject, path) {
|
||||
*
|
||||
* Gathered from right here: https://stackoverflow.com/a/3561711/16048617. Because I don't want to introduce some
|
||||
* library for that.
|
||||
*
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const unsafeRegExpCharacters = /[/\-\\^$*+?.()|[\]{}]/g;
|
||||
const unsafeRegExpCharacters: RegExp = /[/\-\\^$*+?.()|[\]{}]/g;
|
||||
|
||||
/**
|
||||
* Escape all the RegExp syntax-related characters in the following value.
|
||||
* @param {string} value Original value.
|
||||
* @return {string} Resulting value with all needed characters escaped.
|
||||
* @param value Original value.
|
||||
* @return Resulting value with all needed characters escaped.
|
||||
*/
|
||||
export function escapeRegExp(value) {
|
||||
export function escapeRegExp(value: string): string {
|
||||
unsafeRegExpCharacters.lastIndex = 0;
|
||||
return value.replace(unsafeRegExpCharacters, "\\$&");
|
||||
}
|
||||
Reference in New Issue
Block a user