diff --git a/src/lib/booru/TagsUtils.js b/src/lib/booru/TagsUtils.js deleted file mode 100644 index c430c4c..0000000 --- a/src/lib/booru/TagsUtils.js +++ /dev/null @@ -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} Map where key is a tag or alias and value is an actual tag name. - */ -export function buildTagsAndAliasesMap(realAndAliasedTags, realTags) { - /** @type {Map} */ - 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; -} diff --git a/src/lib/booru/scraped/parsing/PostParser.js b/src/lib/booru/scraped/parsing/PostParser.js index bac254d..af6d104 100644 --- a/src/lib/booru/scraped/parsing/PostParser.js +++ b/src/lib/booru/scraped/parsing/PostParser.js @@ -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} */ diff --git a/src/lib/booru/search/QueryLexer.js b/src/lib/booru/search/QueryLexer.ts similarity index 69% rename from src/lib/booru/search/QueryLexer.js rename to src/lib/booru/search/QueryLexer.ts index 07e9c22..76d2d5f 100644 --- a/src/lib/booru/search/QueryLexer.js +++ b/src/lib/booru/search/QueryLexer.ts @@ -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; } diff --git a/src/lib/booru/tag-categories.js b/src/lib/booru/tag-categories.ts similarity index 100% rename from src/lib/booru/tag-categories.js rename to src/lib/booru/tag-categories.ts diff --git a/src/lib/booru/tag-utils.ts b/src/lib/booru/tag-utils.ts new file mode 100644 index 0000000..3270685 --- /dev/null +++ b/src/lib/booru/tag-utils.ts @@ -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 { + const tagsAndAliasesMap: Map = 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; +} diff --git a/src/lib/browser/StorageHelper.js b/src/lib/browser/StorageHelper.js deleted file mode 100644 index 1d4bf26..0000000 --- a/src/lib/browser/StorageHelper.js +++ /dev/null @@ -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} 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): void} callback - */ - subscribe(callback) { - this.#storageArea.onChanged.addListener(callback); - } - - /** - * Unsubscribe from changes in the local storage. - * @param {function(Record): void} callback - */ - unsubscribe(callback) { - this.#storageArea.onChanged.removeListener(callback); - } -} - -export default StorageHelper; diff --git a/src/lib/browser/StorageHelper.ts b/src/lib/browser/StorageHelper.ts new file mode 100644 index 0000000..19c4afd --- /dev/null +++ b/src/lib/browser/StorageHelper.ts @@ -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) => 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(key: string, defaultValue: DefaultType | null = null): Promise { + 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); + } +} diff --git a/src/lib/components/ImageShowFullscreenButton.js b/src/lib/components/ImageShowFullscreenButton.js index 7c0b63a..fc00370 100644 --- a/src/lib/components/ImageShowFullscreenButton.js +++ b/src/lib/components/ImageShowFullscreenButton.js @@ -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"; diff --git a/src/lib/components/MaintenancePopup.js b/src/lib/components/MaintenancePopup.js index 6dc1f1c..3966be1 100644 --- a/src/lib/components/MaintenancePopup.js +++ b/src/lib/components/MaintenancePopup.js @@ -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"; diff --git a/src/lib/components/MaintenanceStatusIcon.js b/src/lib/components/MaintenanceStatusIcon.js index 4c0423d..78ebd57 100644 --- a/src/lib/components/MaintenanceStatusIcon.js +++ b/src/lib/components/MaintenanceStatusIcon.js @@ -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"; diff --git a/src/lib/components/MediaBoxTools.js b/src/lib/components/MediaBoxTools.js index c7afac7..f2de6c5 100644 --- a/src/lib/components/MediaBoxTools.js +++ b/src/lib/components/MediaBoxTools.js @@ -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"; diff --git a/src/lib/components/MediaBoxWrapper.js b/src/lib/components/MediaBoxWrapper.js index 4559fd6..ff0ec34 100644 --- a/src/lib/components/MediaBoxWrapper.js +++ b/src/lib/components/MediaBoxWrapper.js @@ -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"; diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.js index 4c72dd1..3b09f08 100644 --- a/src/lib/components/TagDropdownWrapper.js +++ b/src/lib/components/TagDropdownWrapper.js @@ -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(); diff --git a/src/lib/components/TagsForm.js b/src/lib/components/TagsForm.js index 00cdfbc..5376ca3 100644 --- a/src/lib/components/TagsForm.js +++ b/src/lib/components/TagsForm.js @@ -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 { /** diff --git a/src/lib/components/base/BaseComponent.js b/src/lib/components/base/BaseComponent.js index 3fd982e..04ac8fd 100644 --- a/src/lib/components/base/BaseComponent.js +++ b/src/lib/components/base/BaseComponent.js @@ -1,4 +1,4 @@ -import { bindComponent } from "$lib/components/base/ComponentUtils"; +import { bindComponent } from "$lib/components/base/component-utils"; /** * @abstract diff --git a/src/lib/components/base/ComponentUtils.js b/src/lib/components/base/ComponentUtils.js deleted file mode 100644 index 768621f..0000000 --- a/src/lib/components/base/ComponentUtils.js +++ /dev/null @@ -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; -} diff --git a/src/lib/components/base/component-utils.ts b/src/lib/components/base/component-utils.ts new file mode 100644 index 0000000..636b8f4 --- /dev/null +++ b/src/lib/components/base/component-utils.ts @@ -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; +} diff --git a/src/lib/extension/ConfigurationController.js b/src/lib/extension/ConfigurationController.ts similarity index 62% rename from src/lib/extension/ConfigurationController.js rename to src/lib/extension/ConfigurationController.ts index 7f973ff..860b852 100644 --- a/src/lib/extension/ConfigurationController.js +++ b/src/lib/extension/ConfigurationController.ts @@ -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} 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(settingName: string, defaultValue: DefaultType | null = null): Promise { 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} */ - async writeSetting(settingName, value) { + async writeSetting(settingName: string, value: any): Promise { 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} */ - async deleteSetting(settingName) { + async deleteSetting(settingName: string): Promise { 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} changes */ - const changesSubscriber = changes => { + subscribeToChanges(callback: (record: Record) => 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); diff --git a/src/lib/extension/EntitiesController.ts b/src/lib/extension/EntitiesController.ts index da3f950..8a680e5 100644 --- a/src/lib/extension/EntitiesController.ts +++ b/src/lib/extension/EntitiesController.ts @@ -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) => { + 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); } } diff --git a/src/lib/extension/transporting/validators.js b/src/lib/extension/transporting/validators.js deleted file mode 100644 index 111c1be..0000000 --- a/src/lib/extension/transporting/validators.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Map of validators for each entity. Function should throw the error if validation failed. - * @type {Map 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); -} diff --git a/src/lib/extension/transporting/validators.ts b/src/lib/extension/transporting/validators.ts new file mode 100644 index 0000000..dbb1223 --- /dev/null +++ b/src/lib/extension/transporting/validators.ts @@ -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 = { [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 = (importedObject: ImportableObject) => 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; +}; + +/** + * 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); +} diff --git a/src/lib/utils.js b/src/lib/utils.ts similarity index 56% rename from src/lib/utils.js rename to src/lib/utils.ts index c7a6531..251fd43 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.ts @@ -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, 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, "\\$&"); }