/* * Copyright (C) 2023 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 {JsonSchema} from '../data/json-schema.js'; /** * Utility class to help processing profile conditions. */ export class ProfileConditionsUtil { /** * Creates a new instance. */ constructor() { /** @type {RegExp} */ this._splitPattern = /[,;\s]+/; /** @type {Map<string, {operators: Map<string, import('profile-conditions-util').CreateSchemaFunction>}>} */ this._descriptors = new Map([ [ 'popupLevel', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['equal', this._createSchemaPopupLevelEqual.bind(this)], ['notEqual', this._createSchemaPopupLevelNotEqual.bind(this)], ['lessThan', this._createSchemaPopupLevelLessThan.bind(this)], ['greaterThan', this._createSchemaPopupLevelGreaterThan.bind(this)], ['lessThanOrEqual', this._createSchemaPopupLevelLessThanOrEqual.bind(this)], ['greaterThanOrEqual', this._createSchemaPopupLevelGreaterThanOrEqual.bind(this)] ])) } ], [ 'url', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['matchDomain', this._createSchemaUrlMatchDomain.bind(this)], ['matchRegExp', this._createSchemaUrlMatchRegExp.bind(this)] ])) } ], [ 'modifierKeys', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['are', this._createSchemaModifierKeysAre.bind(this)], ['areNot', this._createSchemaModifierKeysAreNot.bind(this)], ['include', this._createSchemaModifierKeysInclude.bind(this)], ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)] ])) } ], [ 'flags', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['are', this._createSchemaFlagsAre.bind(this)], ['areNot', this._createSchemaFlagsAreNot.bind(this)], ['include', this._createSchemaFlagsInclude.bind(this)], ['notInclude', this._createSchemaFlagsNotInclude.bind(this)] ])) } ] ]); } /** * Creates a new JSON schema descriptor for the given set of condition groups. * @param {import('settings').ProfileConditionGroup[]} conditionGroups An array of condition groups. * For a profile match, all of the items must return successfully in at least one of the groups. * @returns {JsonSchema} A new `JsonSchema` object. */ createSchema(conditionGroups) { const anyOf = []; for (const {conditions} of conditionGroups) { const allOf = []; for (const {type, operator, value} of conditions) { const conditionDescriptor = this._descriptors.get(type); if (typeof conditionDescriptor === 'undefined') { continue; } const createSchema = conditionDescriptor.operators.get(operator); if (typeof createSchema === 'undefined') { continue; } const schema = createSchema(value); allOf.push(schema); } switch (allOf.length) { case 0: break; case 1: anyOf.push(allOf[0]); break; default: anyOf.push({allOf}); break; } } let schema; switch (anyOf.length) { case 0: schema = {}; break; case 1: schema = anyOf[0]; break; default: schema = {anyOf}; break; } return new JsonSchema(schema); } /** * Creates a normalized version of the context object to test, * assigning dependent fields as needed. * @param {import('settings').OptionsContext} context A context object which is used during schema validation. * @returns {import('profile-conditions-util').NormalizedOptionsContext} A normalized context object. */ normalizeContext(context) { const normalizedContext = /** @type {import('profile-conditions-util').NormalizedOptionsContext} */ (Object.assign({}, context)); const {url} = normalizedContext; if (typeof url === 'string') { try { normalizedContext.domain = new URL(url).hostname; } catch (e) { // NOP } } const {flags} = normalizedContext; if (!Array.isArray(flags)) { normalizedContext.flags = []; } return normalizedContext; } // Private /** * @param {string} value * @returns {string[]} */ _split(value) { return value.split(this._splitPattern); } /** * @param {string} value * @returns {number} */ _stringToNumber(value) { const number = Number.parseFloat(value); return Number.isFinite(number) ? number : 0; } // popupLevel schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelEqual(value) { const number = this._stringToNumber(value); return { required: ['depth'], properties: { depth: {const: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelNotEqual(value) { return { not: { anyOf: [this._createSchemaPopupLevelEqual(value)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelLessThan(value) { const number = this._stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', exclusiveMaximum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelGreaterThan(value) { const number = this._stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', exclusiveMinimum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelLessThanOrEqual(value) { const number = this._stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', maximum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaPopupLevelGreaterThanOrEqual(value) { const number = this._stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', minimum: number} } }; } // url schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaUrlMatchDomain(value) { const oneOf = []; for (let domain of this._split(value)) { if (domain.length === 0) { continue; } domain = domain.toLowerCase(); oneOf.push({const: domain}); } return { required: ['domain'], properties: { domain: {oneOf} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaUrlMatchRegExp(value) { return { required: ['url'], properties: { url: {type: 'string', pattern: value, patternFlags: 'i'} } }; } // modifierKeys schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaModifierKeysAre(value) { return this._createSchemaArrayCheck('modifierKeys', value, true, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaModifierKeysAreNot(value) { return { not: { anyOf: [this._createSchemaArrayCheck('modifierKeys', value, true, false)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaModifierKeysInclude(value) { return this._createSchemaArrayCheck('modifierKeys', value, false, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaModifierKeysNotInclude(value) { return this._createSchemaArrayCheck('modifierKeys', value, false, true); } // modifierKeys schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaFlagsAre(value) { return this._createSchemaArrayCheck('flags', value, true, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaFlagsAreNot(value) { return { not: { anyOf: [this._createSchemaArrayCheck('flags', value, true, false)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaFlagsInclude(value) { return this._createSchemaArrayCheck('flags', value, false, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ _createSchemaFlagsNotInclude(value) { return this._createSchemaArrayCheck('flags', value, false, true); } // Generic /** * @param {string} key * @param {string} value * @param {boolean} exact * @param {boolean} none * @returns {import('ext/json-schema').Schema} */ _createSchemaArrayCheck(key, value, exact, none) { /** @type {import('ext/json-schema').Schema[]} */ const containsList = []; for (const item of this._split(value)) { if (item.length === 0) { continue; } containsList.push({ contains: { const: item } }); } const containsListCount = containsList.length; /** @type {import('ext/json-schema').Schema} */ const schema = { type: 'array' }; if (exact) { schema.maxItems = containsListCount; } if (none) { if (containsListCount > 0) { schema.not = {anyOf: containsList}; } } else { schema.minItems = containsListCount; if (containsListCount > 0) { schema.allOf = containsList; } } return { required: [key], properties: { [key]: schema } }; } }