/* * Copyright (C) 2023-2024 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'; /** @type {RegExp} */ const splitPattern = /[,;\s]+/; /** @type {Map<string, {operators: Map<string, import('profile-conditions-util').CreateSchemaFunction>}>} */ const descriptors = new Map([ [ 'popupLevel', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['equal', createSchemaPopupLevelEqual.bind(this)], ['notEqual', createSchemaPopupLevelNotEqual.bind(this)], ['lessThan', createSchemaPopupLevelLessThan.bind(this)], ['greaterThan', createSchemaPopupLevelGreaterThan.bind(this)], ['lessThanOrEqual', createSchemaPopupLevelLessThanOrEqual.bind(this)], ['greaterThanOrEqual', createSchemaPopupLevelGreaterThanOrEqual.bind(this)] ])) } ], [ 'url', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['matchDomain', createSchemaUrlMatchDomain.bind(this)], ['matchRegExp', createSchemaUrlMatchRegExp.bind(this)] ])) } ], [ 'modifierKeys', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['are', createSchemaModifierKeysAre.bind(this)], ['areNot', createSchemaModifierKeysAreNot.bind(this)], ['include', createSchemaModifierKeysInclude.bind(this)], ['notInclude', createSchemaModifierKeysNotInclude.bind(this)] ])) } ], [ 'flags', { operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ ['are', createSchemaFlagsAre.bind(this)], ['areNot', createSchemaFlagsAreNot.bind(this)], ['include', createSchemaFlagsInclude.bind(this)], ['notInclude', 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. */ export function createSchema(conditionGroups) { const anyOf = []; for (const {conditions} of conditionGroups) { const allOf = []; for (const {type, operator, value} of conditions) { const conditionDescriptor = descriptors.get(type); if (typeof conditionDescriptor === 'undefined') { continue; } const createSchema2 = conditionDescriptor.operators.get(operator); if (typeof createSchema2 === 'undefined') { continue; } const schema = createSchema2(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. */ export function 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[]} */ function split(value) { return value.split(splitPattern); } /** * @param {string} value * @returns {number} */ function 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} */ function createSchemaPopupLevelEqual(value) { const number = stringToNumber(value); return { required: ['depth'], properties: { depth: {const: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaPopupLevelNotEqual(value) { return { not: { anyOf: [createSchemaPopupLevelEqual(value)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaPopupLevelLessThan(value) { const number = stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', exclusiveMaximum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaPopupLevelGreaterThan(value) { const number = stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', exclusiveMinimum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaPopupLevelLessThanOrEqual(value) { const number = stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', maximum: number} } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaPopupLevelGreaterThanOrEqual(value) { const number = stringToNumber(value); return { required: ['depth'], properties: { depth: {type: 'number', minimum: number} } }; } // URL schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaUrlMatchDomain(value) { const oneOf = []; for (let domain of 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} */ function 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} */ function createSchemaModifierKeysAre(value) { return createSchemaArrayCheck('modifierKeys', value, true, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaModifierKeysAreNot(value) { return { not: { anyOf: [createSchemaArrayCheck('modifierKeys', value, true, false)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaModifierKeysInclude(value) { return createSchemaArrayCheck('modifierKeys', value, false, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaModifierKeysNotInclude(value) { return createSchemaArrayCheck('modifierKeys', value, false, true); } // modifierKeys schema creation functions /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaFlagsAre(value) { return createSchemaArrayCheck('flags', value, true, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaFlagsAreNot(value) { return { not: { anyOf: [createSchemaArrayCheck('flags', value, true, false)] } }; } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaFlagsInclude(value) { return createSchemaArrayCheck('flags', value, false, false); } /** * @param {string} value * @returns {import('ext/json-schema').Schema} */ function createSchemaFlagsNotInclude(value) { return createSchemaArrayCheck('flags', value, false, true); } // Generic /** * @param {string} key * @param {string} value * @param {boolean} exact * @param {boolean} none * @returns {import('ext/json-schema').Schema} */ function createSchemaArrayCheck(key, value, exact, none) { /** @type {import('ext/json-schema').Schema[]} */ const containsList = []; for (const item of 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 } }; }