Refactor to enforce strictNullChecks
This commit applies `strictNullChecks` to the entire codebase to improve maintainability and type safety. Key changes include: - Remove some explicit null-checks where unnecessary. - Add necessary null-checks. - Refactor static factory functions for a more functional approach. - Improve some test names and contexts for better debugging. - Add unit tests for any additional logic introduced. - Refactor `createPositionFromRegexFullMatch` to its own function as the logic is reused. - Prefer `find` prefix on functions that may return `undefined` and `get` prefix for those that always return a value.
This commit is contained in:
@@ -11,8 +11,16 @@ export default defineConfig({
|
|||||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
baseUrl: `http://localhost:${getApplicationPort()}/`,
|
||||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getApplicationPort(): number {
|
||||||
|
const port = ViteConfig.server?.port;
|
||||||
|
if (port === undefined) {
|
||||||
|
throw new Error('Unknown application port');
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ These checks validate various qualities like runtime execution, building process
|
|||||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
|
||||||
- [`./tests/unit/`](./../tests/unit/)
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
- Stores unit test code.
|
- Stores unit test code.
|
||||||
- The directory structure mirrors [`./src/`](./../src).
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
- [`shared/`](./../tests/unit/shared/)
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
- Contains shared unit test functionalities.
|
- Contains shared unit test functionalities.
|
||||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
|
||||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
- Shared test cases.
|
- Shared test cases.
|
||||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
|
|||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetterType) {
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
|||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Environment = {
|
interface ErrorPrototypeManipulation {
|
||||||
|
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
|
||||||
|
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
|
||||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fixPrototype(target: Error, prototype: CustomError) {
|
function fixPrototype(target: Error, prototype: CustomError) {
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
// This is recommended by TypeScript guidelines.
|
||||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
if (!functionExists(setPrototypeOf)) {
|
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
|
||||||
|
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
|
||||||
|
if (!isFunction(setPrototypeOf)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPrototypeOf(target, prototype);
|
setPrototypeOf(target, prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStackTrace(target: Error) {
|
function ensureStackTrace(target: Error) {
|
||||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
|
||||||
if (!functionExists(captureStackTrace)) {
|
if (!isFunction(captureStackTrace)) {
|
||||||
// captureStackTrace is only available on V8, if it's not available
|
// captureStackTrace is only available on V8, if it's not available
|
||||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||||
return;
|
return;
|
||||||
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
|
|||||||
captureStackTrace(target, target.constructor);
|
captureStackTrace(target, target.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function functionExists(func: unknown): boolean {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
function isFunction(func: unknown): func is Function {
|
||||||
return typeof func === 'function';
|
return typeof func === 'function';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
) {
|
) {
|
||||||
if (value === undefined || value === null) {
|
|
||||||
throw new Error('absent enum value');
|
|
||||||
}
|
|
||||||
if (!(value in enumVariable)) {
|
if (!(value in enumVariable)) {
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
|
|
||||||
public create(language: ScriptingLanguage): T {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
const getter = this.getters.get(language);
|
||||||
|
if (!getter) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
}
|
}
|
||||||
const getter = this.getters.get(language);
|
|
||||||
const instance = getter();
|
const instance = getter();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
if (this.getters.has(language)) {
|
if (this.getters.has(language)) {
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
validateApp(app);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collection = this.app.getCollection(os);
|
const collection = this.app.getCollection(os);
|
||||||
if (!this.collection) {
|
this.collection = collection;
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
|
||||||
}
|
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.states[os],
|
newState: this.states[os],
|
||||||
oldState: this.states[this.currentOs],
|
oldState: this.states[this.currentOs],
|
||||||
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
|
||||||
if (!app) {
|
|
||||||
throw new Error('missing app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
|
|||||||
@@ -10,18 +10,23 @@ export async function buildContext(
|
|||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment.os);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
function getInitialOs(
|
||||||
|
app: IApplication,
|
||||||
|
currentOs: OperatingSystem | undefined,
|
||||||
|
): OperatingSystem {
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
|
return getMostSupportedOs(supportedOsList, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
) {
|
) {
|
||||||
if (!userSelection) { throw new Error('missing userSelection'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
|
||||||
if (!generator) { throw new Error('missing generator'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
return this.scripts.get(script);
|
const position = this.scripts.get(script);
|
||||||
|
if (!position) {
|
||||||
|
throw new Error('Unknown script: Position could not be found for the script');
|
||||||
|
}
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
this.lines.push(...lines);
|
if (lines) {
|
||||||
|
this.lines.push(...lines);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('missing scripts'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
@@ -68,8 +66,19 @@ function appendSelection(
|
|||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const { script } = selection;
|
const { script } = selection;
|
||||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
const scriptCode = getSelectedCode(selection);
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedCode(selection: SelectedScript): string {
|
||||||
|
const { code } = selection.script;
|
||||||
|
if (!selection.revert) {
|
||||||
|
return code.execute;
|
||||||
|
}
|
||||||
|
if (!code.revert) {
|
||||||
|
throw new Error('Reverted script lacks revert code.');
|
||||||
|
}
|
||||||
|
return code.revert;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
import {
|
||||||
|
IFilterChangeDetails, IFilterChangeDetailsVisitor,
|
||||||
|
ApplyFilterAction, ClearFilterAction,
|
||||||
|
} from './IFilterChangeDetails';
|
||||||
|
|
||||||
export class FilterChange implements IFilterChangeDetails {
|
export class FilterChange implements IFilterChangeDetails {
|
||||||
public static forApply(filter: IFilterResult) {
|
public static forApply(
|
||||||
if (!filter) {
|
filter: IFilterResult,
|
||||||
throw new Error('missing filter');
|
): IFilterChangeDetails {
|
||||||
}
|
return new FilterChange({ type: FilterActionType.Apply, filter });
|
||||||
return new FilterChange(FilterActionType.Apply, filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forClear() {
|
public static forClear(): IFilterChangeDetails {
|
||||||
return new FilterChange(FilterActionType.Clear);
|
return new FilterChange({ type: FilterActionType.Clear });
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
|
||||||
public readonly actionType: FilterActionType,
|
|
||||||
public readonly filter?: IFilterResult,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
if (!visitor) {
|
switch (this.action.type) {
|
||||||
throw new Error('missing visitor');
|
|
||||||
}
|
|
||||||
switch (this.actionType) {
|
|
||||||
case FilterActionType.Apply:
|
case FilterActionType.Apply:
|
||||||
visitor.onApply(this.filter);
|
if (visitor.onApply) {
|
||||||
|
visitor.onApply(this.action.filter);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FilterActionType.Clear:
|
case FilterActionType.Clear:
|
||||||
visitor.onClear();
|
if (visitor.onClear) {
|
||||||
|
visitor.onClear();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
throw new Error(`Unknown action: ${this.action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
|
|
||||||
export interface IFilterChangeDetails {
|
export interface IFilterChangeDetails {
|
||||||
readonly actionType: FilterActionType;
|
readonly action: FilterAction;
|
||||||
readonly filter?: IFilterResult;
|
|
||||||
|
|
||||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilterChangeDetailsVisitor {
|
export interface IFilterChangeDetailsVisitor {
|
||||||
onClear(): void;
|
readonly onClear?: () => void;
|
||||||
onApply(filter: IFilterResult): void;
|
readonly onApply?: (filter: IFilterResult) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApplyFilterAction = {
|
||||||
|
readonly type: FilterActionType.Apply,
|
||||||
|
readonly filter: IFilterResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearFilterAction = {
|
||||||
|
readonly type: FilterActionType.Clear,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterAction = ApplyFilterAction | ClearFilterAction;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
|
|||||||
public readonly query: string,
|
public readonly query: string,
|
||||||
) {
|
) {
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
public removeAllInCategory(categoryId: number): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const category = this.collection.getCategory(categoryId);
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
if (!scriptsToRemove.length) {
|
if (!scriptsToRemove.length) {
|
||||||
@@ -57,7 +57,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const scriptsToAddOrUpdate = this.collection
|
const scriptsToAddOrUpdate = this.collection
|
||||||
.findCategory(categoryId)
|
.getCategory(categoryId)
|
||||||
.getAllScriptsRecursively()
|
.getAllScriptsRecursively()
|
||||||
.filter(
|
.filter(
|
||||||
(script) => !this.scripts.exists(script.id)
|
(script) => !this.scripts.exists(script.id)
|
||||||
@@ -74,17 +74,14 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const script = this.collection.getScript(scriptId);
|
||||||
if (!script) {
|
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
|
||||||
}
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
this.scripts.addItem(selectedScript);
|
this.scripts.addItem(selectedScript);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.collection.findScript(scriptId);
|
const script = this.collection.getScript(scriptId);
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
@@ -130,7 +127,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
if (!scripts || scripts.length === 0) {
|
if (!scripts.length) {
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||||
}
|
}
|
||||||
let totalChanged = 0;
|
let totalChanged = 0;
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections?.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
|
||||||
throw new Error('missing collection provided');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ export function parseCategoryCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content.actions.length) {
|
||||||
throw new Error('missing content');
|
|
||||||
}
|
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
CategoryData, ScriptData, CategoryOrScriptData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
@@ -16,7 +16,6 @@ export function parseCategory(
|
|||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
|
|||||||
readonly factory: CategoryFactoryType,
|
readonly factory: CategoryFactoryType,
|
||||||
readonly parentCategory?: CategoryData,
|
readonly parentCategory?: CategoryData,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
|||||||
/* scripts: */ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new NodeValidator({
|
return new NodeValidator({
|
||||||
type: NodeType.Category,
|
type: NodeType.Category,
|
||||||
selfNode: context.categoryData,
|
selfNode: context.categoryData,
|
||||||
parentNode: context.parentCategory,
|
parentNode: context.parentCategory,
|
||||||
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
|
|||||||
.assertDefined(category)
|
.assertDefined(category)
|
||||||
.assertValidName(category.category)
|
.assertValidName(category.category)
|
||||||
.assert(
|
.assert(
|
||||||
() => category.children && category.children.length > 0,
|
() => category.children.length > 0,
|
||||||
`"${category.category}" has no children.`,
|
`"${category.category}" has no children.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
|
|||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData as CategoryData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
factory: context.factory,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subCategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else if (isScript(context.nodeData)) {
|
||||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
const script = parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
validator.throw('Node is neither a category or a script.');
|
validator.throw('Node is neither a category or a script.');
|
||||||
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
const holder = (data as InstructionHolder);
|
return hasCode(data) || hasCall(data);
|
||||||
return hasCode(holder) || hasCall(holder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return hasProperty(data, 'category');
|
return hasProperty(data, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: InstructionHolder): boolean {
|
function hasCode(data: unknown): boolean {
|
||||||
return hasProperty(data, 'code');
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: InstructionHolder) {
|
function hasCall(data: unknown) {
|
||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
|
||||||
throw new Error('missing documentable');
|
|
||||||
}
|
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class NodeValidator {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public throw(errorMessage: string) {
|
public throw(errorMessage: string): never {
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
|||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
) {
|
) {
|
||||||
if (!scripting) { throw new Error('missing scripting'); }
|
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ export class Expression implements IExpression {
|
|||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
) {
|
) {
|
||||||
if (!position) {
|
|
||||||
throw new Error('missing position');
|
|
||||||
}
|
|
||||||
if (!evaluator) {
|
|
||||||
throw new Error('missing evaluator');
|
|
||||||
}
|
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
|
||||||
throw new Error('missing context');
|
|
||||||
}
|
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
|||||||
@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
|||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
) {
|
) {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
|
export function createPositionFromRegexFullMatch(
|
||||||
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
if (startPos === undefined) {
|
||||||
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
if (!fullMatch.length) {
|
||||||
|
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const endPos = startPos + fullMatch.length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
public compileExpressions(
|
public compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): string {
|
): string {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return '';
|
||||||
}
|
}
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): void {
|
): void {
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
if (!usedParameterNames?.length) {
|
if (!usedParameterNames.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notProvidedParameters = usedParameterNames
|
const notProvidedParameters = usedParameterNames
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
|||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ const Parsers = [
|
|||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (!leafs) {
|
if (!leafs.length) {
|
||||||
throw new Error('missing leafs');
|
throw new Error('missing leafs');
|
||||||
}
|
}
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
|
||||||
throw new Error('missing leaf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionParser } from '../IExpressionParser';
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
|
||||||
import { IExpression } from '../../Expression/IExpression';
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
const matches = code.matchAll(this.regex);
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||||
const parameters = createParameters(primitiveExpression);
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
yield expression;
|
||||||
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: IPrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
): FunctionParameterCollection {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function hasLines(text: string) {
|
|||||||
*/
|
*/
|
||||||
function inlineComments(code: string): string {
|
function inlineComments(code: string): string {
|
||||||
const makeInlineComment = (comment: string) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = comment?.trim();
|
const value = comment.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
if (!pipes) {
|
|
||||||
throw new Error('missing pipes');
|
|
||||||
}
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
|
||||||
throw new Error('missing pipe in list');
|
|
||||||
}
|
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
if (!this.pipes.has(pipeName)) {
|
const pipe = this.pipes.get(pipeName);
|
||||||
|
if (!pipe) {
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
}
|
}
|
||||||
return this.pipes.get(pipeName);
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export class WithParser implements IExpressionParser {
|
export class WithParser implements IExpressionParser {
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
@@ -42,31 +43,25 @@ function parseAllWithExpressions(
|
|||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.Start,
|
type: WithStatementType.Start,
|
||||||
parameterName: match[1],
|
parameterName: match[1],
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.End,
|
type: WithStatementType.End,
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.ContextVariable,
|
type: WithStatementType.ContextVariable,
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
pipeline: match[1],
|
pipeline: match[1],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return expressions;
|
return expressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WithStatementBuilder {
|
class WithStatementBuilder {
|
||||||
private readonly contextVariables = new Array<{
|
private readonly contextVariables = new Array<{
|
||||||
readonly positionInScope: ExpressionPosition;
|
readonly positionInScope: ExpressionPosition;
|
||||||
@@ -125,7 +120,7 @@ class WithStatementBuilder {
|
|||||||
|
|
||||||
private substituteContextVariables(
|
private substituteContextVariables(
|
||||||
scope: string,
|
scope: string,
|
||||||
substituter: (pipeline: string) => string,
|
substituter: (pipeline?: string) => string,
|
||||||
): string {
|
): string {
|
||||||
if (!this.contextVariables.length) {
|
if (!this.contextVariables.length) {
|
||||||
return scope;
|
return scope;
|
||||||
@@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] {
|
|||||||
.sort((a, b) => b.position.start - a.position.start);
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
const expressions = new Array<IExpression>();
|
const expressions = new Array<IExpression>();
|
||||||
const builders = new Array<WithStatementBuilder>();
|
const builders = new Array<WithStatementBuilder>();
|
||||||
const throwWithContext = (message: string) => {
|
const throwWithContext = (message: string): never => {
|
||||||
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
||||||
};
|
};
|
||||||
while (sortedStatements.length > 0) {
|
while (sortedStatements.length > 0) {
|
||||||
@@ -178,12 +173,15 @@ function parseWithExpressions(input: string): IExpression[] {
|
|||||||
}
|
}
|
||||||
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
||||||
break;
|
break;
|
||||||
case WithStatementType.End:
|
case WithStatementType.End: {
|
||||||
if (builders.length === 0) {
|
const builder = builders.pop();
|
||||||
|
if (!builder) {
|
||||||
throwWithContext('Redundant `end` statement, missing `with`?');
|
throwWithContext('Redundant `end` statement, missing `with`?');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
expressions.push(builders.pop().buildExpression(statement.position, input));
|
expressions.push(builder.buildExpression(statement.position, input));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (builders.length > 0) {
|
if (builders.length > 0) {
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
|
||||||
throw new Error('missing argument');
|
|
||||||
}
|
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
|
|||||||
|
|
||||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||||
if (!codeSegments?.length) {
|
if (!codeSegments.length) {
|
||||||
throw new Error('missing segments');
|
throw new Error('missing segments');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
revertCode: joinCodeParts(
|
||||||
|
codeSegments
|
||||||
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||||
return codeSegments
|
return codeSegments
|
||||||
.filter((segment) => segment?.length > 0)
|
.filter((segment) => segment.length > 0)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
|||||||
calls: readonly FunctionCall[],
|
calls: readonly FunctionCall[],
|
||||||
functions: ISharedFunctionCollection,
|
functions: ISharedFunctionCollection,
|
||||||
): CompiledCode {
|
): CompiledCode {
|
||||||
if (!functions) { throw new Error('missing functions'); }
|
if (!calls.length) { throw new Error('missing calls'); }
|
||||||
if (!calls?.length) { throw new Error('missing calls'); }
|
|
||||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
|
||||||
const context: FunctionCallCompilationContext = {
|
const context: FunctionCallCompilationContext = {
|
||||||
allFunctions: functions,
|
allFunctions: functions,
|
||||||
rootCallSequence: calls,
|
rootCallSequence: calls,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.code !== undefined;
|
return func.body.type === FunctionBodyType.Code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
calledFunction: ISharedFunction,
|
calledFunction: ISharedFunction,
|
||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
|
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||||
|
throw new Error([
|
||||||
|
'Unexpected function body type.',
|
||||||
|
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||||
|
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||||
|
'Function:',
|
||||||
|
`\t${JSON.stringify(callToFunction)}`,
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
const { code } = calledFunction.body;
|
const { code } = calledFunction.body;
|
||||||
const { args } = callToFunction;
|
const { args } = callToFunction;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
revertCode: (() => {
|
||||||
|
if (!code.revert) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.expressionsCompiler.compileExpressions(code.revert, args);
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.calls !== undefined;
|
return func.body.type === FunctionBodyType.Calls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
context: FunctionCallCompilationContext,
|
context: FunctionCallCompilationContext,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
const nestedCalls = calledFunction.body.calls;
|
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
|
||||||
return nestedCalls.map((nestedCall) => {
|
return nestedCalls.map((nestedCall) => {
|
||||||
try {
|
try {
|
||||||
const compiledParentCall = this.argumentCompiler
|
const compiledParentCall = this.argumentCompiler
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
|||||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||||
|
|
||||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||||
if (calls === undefined) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const sequence = getCallSequence(calls);
|
const sequence = getCallSequence(calls);
|
||||||
return sequence.map((call) => parseFunctionCall(call));
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
}
|
}
|
||||||
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
|||||||
if (calls instanceof Array) {
|
if (calls instanceof Array) {
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
return [calls as FunctionCallData];
|
const singleCall = calls;
|
||||||
|
return [singleCall];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||||
if (!call) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const callArgs = parseArgs(call.parameters);
|
const callArgs = parseArgs(call.parameters);
|
||||||
return new ParsedFunctionCall(call.function, callArgs);
|
return new ParsedFunctionCall(call.function, callArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(
|
function parseArgs(
|
||||||
parameters: FunctionCallParametersData,
|
parameters: FunctionCallParametersData | undefined,
|
||||||
): FunctionCallArgumentCollection {
|
): FunctionCallArgumentCollection {
|
||||||
return Object.keys(parameters || {})
|
const parametersMap = parameters ?? {};
|
||||||
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
return Object.keys(parametersMap)
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
|
||||||
.reduce((args, arg) => {
|
.reduce((args, arg) => {
|
||||||
args.addArgument(arg);
|
args.addArgument(arg);
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
|
|||||||
if (!functionName) {
|
if (!functionName) {
|
||||||
throw new Error('missing function name in function call');
|
throw new Error('missing function name in function call');
|
||||||
}
|
}
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
|
|||||||
export interface ISharedFunction {
|
export interface ISharedFunction {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
readonly body: ISharedFunctionBody;
|
readonly body: SharedFunctionBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISharedFunctionBody {
|
export interface CallFunctionBody {
|
||||||
readonly type: FunctionBodyType;
|
readonly type: FunctionBodyType.Calls,
|
||||||
readonly code: IFunctionCode | undefined;
|
readonly calls: readonly FunctionCall[],
|
||||||
readonly calls: readonly FunctionCall[] | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeFunctionBody {
|
||||||
|
readonly type: FunctionBodyType.Code;
|
||||||
|
readonly code: IFunctionCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
|
||||||
|
|
||||||
export enum FunctionBodyType {
|
export enum FunctionBodyType {
|
||||||
Code,
|
Code,
|
||||||
Calls,
|
Calls,
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
if (!parameter) {
|
|
||||||
throw new Error('missing parameter');
|
|
||||||
}
|
|
||||||
if (this.includesName(parameter.name)) {
|
if (this.includesName(parameter.name)) {
|
||||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FunctionCall } from './Call/FunctionCall';
|
import { FunctionCall } from './Call/FunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export function createCallerFunction(
|
|||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
callSequence: readonly FunctionCall[],
|
callSequence: readonly FunctionCall[],
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
if (!callSequence || !callSequence.length) {
|
if (!callSequence.length) {
|
||||||
throw new Error(`missing call sequence in function "${name}"`);
|
throw new Error(`missing call sequence in function "${name}"`);
|
||||||
}
|
}
|
||||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||||
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SharedFunction implements ISharedFunction {
|
class SharedFunction implements ISharedFunction {
|
||||||
public readonly body: ISharedFunctionBody;
|
public readonly body: SharedFunctionBody;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
|
|||||||
bodyType: FunctionBodyType,
|
bodyType: FunctionBodyType,
|
||||||
) {
|
) {
|
||||||
if (!name) { throw new Error('missing function name'); }
|
if (!name) { throw new Error('missing function name'); }
|
||||||
if (!parameters) { throw new Error('missing parameters'); }
|
|
||||||
this.body = {
|
switch (bodyType) {
|
||||||
type: bodyType,
|
case FunctionBodyType.Code:
|
||||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
this.body = {
|
||||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
type: FunctionBodyType.Code,
|
||||||
};
|
code: content as IFunctionCode,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FunctionBodyType.Calls:
|
||||||
|
this.body = {
|
||||||
|
type: FunctionBodyType.Calls,
|
||||||
|
calls: content as readonly FunctionCall[],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||||
|
|
||||||
public addFunction(func: ISharedFunction): void {
|
public addFunction(func: ISharedFunction): void {
|
||||||
if (!func) { throw new Error('missing function'); }
|
|
||||||
if (this.has(func.name)) {
|
if (this.has(func.name)) {
|
||||||
throw new Error(`function with name ${func.name} already exists`);
|
throw new Error(`function with name ${func.name} already exists`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type {
|
||||||
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
||||||
|
} from '@/application/collections/';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
@@ -55,16 +56,18 @@ function parseFunction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(
|
function validateCode(
|
||||||
data: FunctionData,
|
data: CodeFunctionData,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
validator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
): void {
|
): void {
|
||||||
[data.code, data.revertCode].forEach(
|
[data.code, data.revertCode]
|
||||||
(code) => validator.throwIfInvalid(
|
.filter((code): code is string => Boolean(code))
|
||||||
code,
|
.forEach(
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
(code) => validator.throwIfInvalid(
|
||||||
),
|
code,
|
||||||
);
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
|
|||||||
}, new FunctionParameterCollection());
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||||
return Boolean(data.code);
|
return (data as CodeInstruction).code !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: FunctionData): boolean {
|
function hasCall(data: FunctionData): data is CallFunctionData {
|
||||||
return Boolean(data.call);
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
ensureNoUndefinedItem(functions);
|
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
ensureExpectedParametersType(functions);
|
ensureExpectedParametersType(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
|
|||||||
return `"${list.join('","')}"`;
|
return `"${list.join('","')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||||
// Ensure functions do not define both call and code
|
// Ensure functions do not define both call and code
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
if (withBothCallAndCode.length) {
|
if (withBothCallAndCode.length) {
|
||||||
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
|
|||||||
&& value.every((item) => typeof item === 'object');
|
&& value.every((item) => typeof item === 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
function printNames(holders: readonly InstructionHolder[]) {
|
function printNames(holders: readonly FunctionData[]) {
|
||||||
return printList(holders.map((holder) => holder.name));
|
return printList(holders.map((holder) => holder.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
|
||||||
if (functions.some((func) => !func)) {
|
|
||||||
throw new Error('some functions are undefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
const duplicateCodes = getDuplicates(functions
|
const callFunctions = functions
|
||||||
|
.filter((func) => hasCode(func))
|
||||||
|
.map((func) => func as CodeFunctionData);
|
||||||
|
const duplicateCodes = getDuplicates(callFunctions
|
||||||
.map((func) => func.code)
|
.map((func) => func.code)
|
||||||
.filter((code) => code));
|
.filter((code) => code));
|
||||||
if (duplicateCodes.length > 0) {
|
if (duplicateCodes.length > 0) {
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
}
|
}
|
||||||
const duplicateRevertCodes = getDuplicates(functions
|
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||||
.filter((func) => func.revertCode)
|
.map((func) => func.revertCode)
|
||||||
.map((func) => func.revertCode));
|
.filter((code): code is string => Boolean(code)));
|
||||||
if (duplicateRevertCodes.length > 0) {
|
if (duplicateRevertCodes.length > 0) {
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script) { throw new Error('missing script'); }
|
return hasCall(script);
|
||||||
if (!script.call) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
if (!script) { throw new Error('missing script'); }
|
|
||||||
try {
|
try {
|
||||||
|
if (!hasCall(script)) {
|
||||||
|
throw new Error('Script does include any calls.');
|
||||||
|
}
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
validateCompiledCode(compiledCode, this.codeValidator);
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
[compiledCode.code, compiledCode.revertCode]
|
||||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
.filter((code): code is string => Boolean(code))
|
||||||
);
|
.map((code) => code as string)
|
||||||
|
.forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
||||||
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
|
|||||||
import { CodeValidator } from './Validation/CodeValidator';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
@@ -24,7 +23,6 @@ export function parseScript(
|
|||||||
): Script {
|
): Script {
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = scriptFactory(
|
||||||
/* name: */ data.name,
|
/* name: */ data.name,
|
||||||
@@ -34,12 +32,12 @@ export function parseScript(
|
|||||||
);
|
);
|
||||||
return script;
|
return script;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
validator.throw(err.message);
|
return validator.throw(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string,
|
level: string | undefined,
|
||||||
parser: IEnumParser<RecommendationLevel>,
|
parser: IEnumParser<RecommendationLevel>,
|
||||||
): RecommendationLevel | undefined {
|
): RecommendationLevel | undefined {
|
||||||
if (!level) {
|
if (!level) {
|
||||||
@@ -56,39 +54,45 @@ function parseCode(
|
|||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const code = new ScriptCode(script.code, script.revertCode);
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
|
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: ScriptCode,
|
||||||
codeValidator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
[scriptCode.execute, scriptCode.revert].forEach(
|
[scriptCode.execute, scriptCode.revert]
|
||||||
(code) => codeValidator.throwIfInvalid(
|
.filter((code): code is string => Boolean(code))
|
||||||
code,
|
.forEach(
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
(code) => validator.throwIfInvalid(
|
||||||
),
|
code,
|
||||||
);
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
function validateScript(
|
||||||
|
script: ScriptData,
|
||||||
|
validator: NodeValidator,
|
||||||
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator
|
validator
|
||||||
.assertDefined(script)
|
.assertDefined(script)
|
||||||
.assertValidName(script.name)
|
.assertValidName(script.name)
|
||||||
.assert(
|
.assert(
|
||||||
() => Boolean(script.code || script.call),
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
'Must define either "call" or "code".',
|
'Neither "call" or "code" is defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.code && script.call),
|
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||||
'Cannot define both "call" and "code".',
|
'Both "call" and "code" are defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.revertCode && script.call),
|
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||||
'Cannot define "revertCode" if "call" is defined.',
|
'Both "call" and "revertCode" are defined.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
|
|||||||
code: string,
|
code: string,
|
||||||
rules: readonly ICodeValidationRule[],
|
rules: readonly ICodeValidationRule[],
|
||||||
): void {
|
): void {
|
||||||
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
|
|||||||
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
export class NoDuplicatedLines implements ICodeValidationRule {
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
constructor(private readonly syntax: ILanguageSyntax) {
|
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
|
|
||||||
public substitute(code: string, info: IProjectInformation): string {
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
if (!code) { throw new Error('missing code'); }
|
if (!code) { throw new Error('missing code'); }
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
const args = new FunctionCallArgumentCollection();
|
const args = new FunctionCallArgumentCollection();
|
||||||
const substitute = (name: string, value: string) => args
|
const substitute = (name: string, value: string) => args
|
||||||
.addArgument(new FunctionCallArgument(name, value));
|
.addArgument(new FunctionCallArgument(name, value));
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
|
|||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
): IScriptingDefinition {
|
): IScriptingDefinition {
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
if (!definition) { throw new Error('missing definition'); }
|
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||||
|
|||||||
37
src/application/collections/collection.yaml.d.ts
vendored
37
src/application/collections/collection.yaml.d.ts
vendored
@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
export type DocumentationData = ReadonlyArray<string> | string;
|
export type DocumentationData = ReadonlyArray<string> | string | undefined;
|
||||||
|
|
||||||
export interface DocumentableData {
|
export interface DocumentableData {
|
||||||
readonly docs?: DocumentationData;
|
readonly docs?: DocumentationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstructionHolder {
|
export interface CodeInstruction {
|
||||||
readonly name: string;
|
readonly code: string;
|
||||||
|
|
||||||
readonly code?: string;
|
|
||||||
readonly revertCode?: string;
|
readonly revertCode?: string;
|
||||||
|
|
||||||
readonly call?: FunctionCallsData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CallInstruction {
|
||||||
|
readonly call: FunctionCallsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstructionHolder = CodeInstruction | CallInstruction;
|
||||||
|
|
||||||
export interface ParameterDefinitionData {
|
export interface ParameterDefinitionData {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly optional?: boolean;
|
readonly optional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionData extends InstructionHolder {
|
export type FunctionDefinition = {
|
||||||
|
readonly name: string;
|
||||||
readonly parameters?: readonly ParameterDefinitionData[];
|
readonly parameters?: readonly ParameterDefinitionData[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallFunctionData = FunctionDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type FunctionData = CodeFunctionData | CallFunctionData;
|
||||||
|
|
||||||
export interface FunctionCallParametersData {
|
export interface FunctionCallParametersData {
|
||||||
readonly [index: string]: string;
|
readonly [index: string]: string;
|
||||||
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
|
|||||||
|
|
||||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
export type ScriptDefinition = DocumentableData & {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly recommend?: string;
|
readonly recommend?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeScriptData = ScriptDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallScriptData = ScriptDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type ScriptData = CodeScriptData | CallScriptData;
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
readonly language: string;
|
readonly language: string;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export class Application implements IApplication {
|
|||||||
public info: IProjectInformation,
|
public info: IProjectInformation,
|
||||||
public collections: readonly ICategoryCollection[],
|
public collections: readonly ICategoryCollection[],
|
||||||
) {
|
) {
|
||||||
validateInformation(info);
|
|
||||||
validateCollections(collections);
|
validateCollections(collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,19 +15,17 @@ export class Application implements IApplication {
|
|||||||
return this.collections.map((collection) => collection.os);
|
return this.collections.map((collection) => collection.os);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
|
||||||
return this.collections.find((collection) => collection.os === operatingSystem);
|
const collection = this.collections.find((c) => c.os === operatingSystem);
|
||||||
}
|
if (!collection) {
|
||||||
}
|
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
|
||||||
|
}
|
||||||
function validateInformation(info: IProjectInformation) {
|
return collection;
|
||||||
if (!info) {
|
|
||||||
throw new Error('missing project information');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||||
if (!collections || !collections.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.filter((c) => !c).length > 0) {
|
if (collections.filter((c) => !c).length > 0) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly docs: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>,
|
public readonly scripts: ReadonlyArray<IScript>,
|
||||||
) {
|
) {
|
||||||
super(id);
|
super(id);
|
||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
@@ -39,10 +39,7 @@ function validateCategory(category: ICategory) {
|
|||||||
if (!category.name) {
|
if (!category.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (
|
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
||||||
(!category.subCategories || category.subCategories.length === 0)
|
|
||||||
&& (!category.scripts || category.scripts.length === 0)
|
|
||||||
) {
|
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
public readonly actions: ReadonlyArray<ICategory>,
|
public readonly actions: ReadonlyArray<ICategory>,
|
||||||
public readonly scripting: IScriptingDefinition,
|
public readonly scripting: IScriptingDefinition,
|
||||||
) {
|
) {
|
||||||
if (!scripting) {
|
|
||||||
throw new Error('missing scripting definition');
|
|
||||||
}
|
|
||||||
this.queryable = makeQueryable(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
assertInRange(os, OperatingSystem);
|
assertInRange(os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
@@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public getCategory(categoryId: number): ICategory {
|
||||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||||
|
if (!category) {
|
||||||
|
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||||
|
}
|
||||||
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
assertInRange(level, RecommendationLevel);
|
assertInRange(level, RecommendationLevel);
|
||||||
return this.queryable.scriptsByLevel.get(level);
|
const scripts = this.queryable.scriptsByLevel.get(level);
|
||||||
|
return scripts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
public getScript(scriptId: string): IScript {
|
||||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||||
|
if (!script) {
|
||||||
|
throw new Error(`missing script: ${scriptId}`);
|
||||||
|
}
|
||||||
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
public getAllScripts(): IScript[] {
|
||||||
@@ -78,13 +84,13 @@ function ensureValid(application: IQueryableCollection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||||
if (!allCategories || allCategories.length === 0) {
|
if (!allCategories.length) {
|
||||||
throw new Error('must consist of at least one category');
|
throw new Error('must consist of at least one category');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||||
if (!allScripts || allScripts.length === 0) {
|
if (!allScripts.length) {
|
||||||
throw new Error('must consist of at least one script');
|
throw new Error('must consist of at least one script');
|
||||||
}
|
}
|
||||||
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export interface IApplication {
|
|||||||
readonly collections: readonly ICategoryCollection[];
|
readonly collections: readonly ICategoryCollection[];
|
||||||
|
|
||||||
getSupportedOsList(): OperatingSystem[];
|
getSupportedOsList(): OperatingSystem[];
|
||||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
getCollection(operatingSystem: OperatingSystem): ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
|
|||||||
export interface ICategory extends IEntity<number>, IDocumentable {
|
export interface ICategory extends IEntity<number>, IDocumentable {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts: ReadonlyArray<IScript>;
|
||||||
includes(script: IScript): boolean;
|
includes(script: IScript): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface ICategoryCollection {
|
|||||||
readonly actions: ReadonlyArray<ICategory>;
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
getCategory(categoryId: number): ICategory;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
getScript(scriptId: string): IScript;
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
getAllCategories(): ReadonlyArray<ICategory>;
|
getAllCategories(): ReadonlyArray<ICategory>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IScriptCode {
|
export interface IScriptCode {
|
||||||
readonly execute: string;
|
readonly execute: string;
|
||||||
readonly revert: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('name is undefined');
|
throw new Error('name is undefined');
|
||||||
}
|
}
|
||||||
if (!version) {
|
|
||||||
throw new Error('undefined version');
|
|
||||||
}
|
|
||||||
if (!slogan) {
|
if (!slogan) {
|
||||||
throw new Error('undefined slogan');
|
throw new Error('undefined slogan');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
public readonly level?: RecommendationLevel,
|
public readonly level?: RecommendationLevel,
|
||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
|
||||||
throw new Error('missing code');
|
|
||||||
}
|
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
export class ScriptCode implements IScriptCode {
|
export class ScriptCode implements IScriptCode {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string | undefined,
|
||||||
) {
|
) {
|
||||||
validateCode(execute);
|
validateCode(execute);
|
||||||
validateRevertCode(revert, execute);
|
validateRevertCode(revert, execute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string) {
|
function validateRevertCode(revertCode: string | undefined, execute: string) {
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ export class CodeRunner {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly system = getWindowInjectedSystemOperations(),
|
private readonly system = getWindowInjectedSystemOperations(),
|
||||||
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
) {
|
) { }
|
||||||
if (!system) {
|
|
||||||
throw new Error('missing system operations');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||||
const { os } = this.environment;
|
const { os } = this.environment;
|
||||||
|
if (os === undefined) {
|
||||||
|
throw new Error('Unidentified operating system');
|
||||||
|
}
|
||||||
const dir = this.system.location.combinePaths(
|
const dir = this.system.location.combinePaths(
|
||||||
this.system.operatingSystem.getTempDirectory(),
|
this.system.operatingSystem.getTempDirectory(),
|
||||||
folderName,
|
folderName,
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { IEnvironmentVariables } from './IEnvironmentVariables';
|
|||||||
|
|
||||||
/* Validation is externalized to keep the environment objects simple */
|
/* Validation is externalized to keep the environment objects simple */
|
||||||
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||||
if (!environment) {
|
|
||||||
throw new Error('missing environment');
|
|
||||||
}
|
|
||||||
const keyValues = capturePropertyValues(environment);
|
const keyValues = capturePropertyValues(environment);
|
||||||
if (!Object.keys(keyValues).length) {
|
if (!Object.keys(keyValues).length) {
|
||||||
throw new Error('Unable to capture key/value pairs');
|
throw new Error('Unable to capture key/value pairs');
|
||||||
@@ -30,7 +27,7 @@ function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[]
|
|||||||
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||||
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||||
*/
|
*/
|
||||||
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
function capturePropertyValues(instance: object): Record<string, unknown> {
|
||||||
const obj: Record<string, unknown> = {};
|
const obj: Record<string, unknown> = {};
|
||||||
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
public register(subscriptions: IEventSubscription[]) {
|
public register(subscriptions: IEventSubscription[]) {
|
||||||
if (!subscriptions || subscriptions.length === 0) {
|
if (subscriptions.length === 0) {
|
||||||
throw new Error('missing subscriptions');
|
throw new Error('missing subscriptions');
|
||||||
}
|
}
|
||||||
if (subscriptions.some((subscription) => !subscription)) {
|
|
||||||
throw new Error('missing subscription in list');
|
|
||||||
}
|
|
||||||
this.subscriptions.push(...subscriptions);
|
this.subscriptions.push(...subscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { ILogger } from './ILogger';
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
export class ConsoleLogger implements ILogger {
|
export class ConsoleLogger implements ILogger {
|
||||||
constructor(private readonly globalConsole: Partial<Console> = console) {
|
constructor(private readonly consoleProxy: Partial<Console> = console) {
|
||||||
if (!globalConsole) {
|
if (!consoleProxy) { // do not trust strictNullChecks for global objects
|
||||||
throw new Error('missing console');
|
throw new Error('missing console');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public info(...params: unknown[]): void {
|
public info(...params: unknown[]): void {
|
||||||
this.globalConsole.info(...params);
|
const logFunction = this.consoleProxy?.info;
|
||||||
|
if (!logFunction) {
|
||||||
|
throw new Error('missing "info" function');
|
||||||
|
}
|
||||||
|
logFunction.call(this.consoleProxy, ...params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
|||||||
throw new Error('missing logger');
|
throw new Error('missing logger');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
info: (...params) => logger.info(...params),
|
info: (...params) => {
|
||||||
|
if (!logger.info) {
|
||||||
|
throw new Error('missing "info" function');
|
||||||
|
}
|
||||||
|
logger.info(...params);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { ILogger } from './ILogger';
|
|||||||
export class WindowInjectedLogger implements ILogger {
|
export class WindowInjectedLogger implements ILogger {
|
||||||
private readonly logger: ILogger;
|
private readonly logger: ILogger;
|
||||||
|
|
||||||
constructor(windowVariables: WindowVariables = window) {
|
constructor(windowVariables: WindowVariables | undefined | null = window) {
|
||||||
if (!windowVariables) {
|
if (!windowVariables) { // do not trust strict null checks for global objects
|
||||||
throw new Error('missing window');
|
throw new Error('missing window');
|
||||||
}
|
}
|
||||||
if (!windowVariables.log) {
|
if (!windowVariables.log) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { IEntity } from '../Entity/IEntity';
|
|||||||
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
readonly length: number;
|
readonly length: number;
|
||||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||||
getById(id: TKey): TEntity | undefined;
|
getById(id: TKey): TEntity;
|
||||||
addItem(item: TEntity): void;
|
addItem(item: TEntity): void;
|
||||||
addOrUpdateItem(item: TEntity): void;
|
addOrUpdateItem(item: TEntity): void;
|
||||||
removeItem(id: TKey): void;
|
removeItem(id: TKey): void;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ implements IRepository<TKey, TEntity> {
|
|||||||
private readonly items: TEntity[];
|
private readonly items: TEntity[];
|
||||||
|
|
||||||
constructor(items?: TEntity[]) {
|
constructor(items?: TEntity[]) {
|
||||||
this.items = items || new Array<TEntity>();
|
this.items = items ?? new Array<TEntity>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get length(): number {
|
public get length(): number {
|
||||||
@@ -17,18 +17,15 @@ implements IRepository<TKey, TEntity> {
|
|||||||
return predicate ? this.items.filter(predicate) : this.items;
|
return predicate ? this.items.filter(predicate) : this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getById(id: TKey): TEntity | undefined {
|
public getById(id: TKey): TEntity {
|
||||||
const items = this.getItems((entity) => entity.id === id);
|
const items = this.getItems((entity) => entity.id === id);
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return undefined;
|
throw new Error(`missing item: ${id}`);
|
||||||
}
|
}
|
||||||
return items[0];
|
return items[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public addItem(item: TEntity): void {
|
public addItem(item: TEntity): void {
|
||||||
if (!item) {
|
|
||||||
throw new Error('missing item');
|
|
||||||
}
|
|
||||||
if (this.exists(item.id)) {
|
if (this.exists(item.id)) {
|
||||||
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
||||||
}
|
}
|
||||||
@@ -36,9 +33,6 @@ implements IRepository<TKey, TEntity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateItem(item: TEntity): void {
|
public addOrUpdateItem(item: TEntity): void {
|
||||||
if (!item) {
|
|
||||||
throw new Error('missing item');
|
|
||||||
}
|
|
||||||
if (this.exists(item.id)) {
|
if (this.exists(item.id)) {
|
||||||
this.removeItem(item.id);
|
this.removeItem(item.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export class DetectorBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private detect(userAgent: string): OperatingSystem {
|
private detect(userAgent: string): OperatingSystem | undefined {
|
||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
throw new Error('missing userAgent');
|
return undefined;
|
||||||
}
|
}
|
||||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class RuntimeEnvironment implements IRuntimeEnvironment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserAgent(window: Partial<Window>): string {
|
function getUserAgent(window: Partial<Window>): string | undefined {
|
||||||
return window?.navigator?.userAgent;
|
return window?.navigator?.userAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export abstract class FactoryValidator<T> implements ISanityValidator {
|
|||||||
private readonly factory: FactoryFunction<T>;
|
private readonly factory: FactoryFunction<T>;
|
||||||
|
|
||||||
protected constructor(factory: FactoryFunction<T>) {
|
protected constructor(factory: FactoryFunction<T>) {
|
||||||
if (!factory) {
|
|
||||||
throw new Error('missing factory');
|
|
||||||
}
|
|
||||||
this.factory = factory;
|
this.factory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export function validateRuntimeSanity(
|
|||||||
options: ISanityCheckOptions,
|
options: ISanityCheckOptions,
|
||||||
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
validators: readonly ISanityValidator[] = DefaultSanityValidators,
|
||||||
): void {
|
): void {
|
||||||
validateContext(options, validators);
|
if (!validators.length) {
|
||||||
|
throw new Error('missing validators');
|
||||||
|
}
|
||||||
const errorMessages = validators.reduce((errors, validator) => {
|
const errorMessages = validators.reduce((errors, validator) => {
|
||||||
if (validator.shouldValidate(options)) {
|
if (validator.shouldValidate(options)) {
|
||||||
const errorMessage = getErrorMessage(validator);
|
const errorMessage = getErrorMessage(validator);
|
||||||
@@ -26,21 +28,6 @@ export function validateRuntimeSanity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateContext(
|
|
||||||
options: ISanityCheckOptions,
|
|
||||||
validators: readonly ISanityValidator[],
|
|
||||||
) {
|
|
||||||
if (!options) {
|
|
||||||
throw new Error('missing options');
|
|
||||||
}
|
|
||||||
if (!validators?.length) {
|
|
||||||
throw new Error('missing validators');
|
|
||||||
}
|
|
||||||
if (validators.some((validator) => !validator)) {
|
|
||||||
throw new Error('missing validator in validators');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
function getErrorMessage(validator: ISanityValidator): string | undefined {
|
||||||
const errorMessages = [...validator.collectErrors()];
|
const errorMessages = [...validator.collectErrors()];
|
||||||
if (!errorMessages.length) {
|
if (!errorMessages.length) {
|
||||||
|
|||||||
@@ -6,17 +6,21 @@ export enum FileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SaveFileDialog {
|
export class SaveFileDialog {
|
||||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
public static saveFile(
|
||||||
const mimeType = this.mimeTypes.get(type);
|
text: string,
|
||||||
|
fileName: string,
|
||||||
|
type: FileType,
|
||||||
|
): void {
|
||||||
|
const mimeType = this.mimeTypes[type];
|
||||||
this.saveBlob(text, mimeType, fileName);
|
this.saveBlob(text, mimeType, fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly mimeTypes = new Map<FileType, string>([
|
private static readonly mimeTypes: Record<FileType, string> = {
|
||||||
// Some browsers (including firefox + IE) require right mime type
|
// Some browsers (including firefox + IE) require right mime type
|
||||||
// otherwise they ignore extension and save the file as text.
|
// otherwise they ignore extension and save the file as text.
|
||||||
[FileType.BatchFile, 'application/bat'], // https://en.wikipedia.org/wiki/Batch_file
|
[FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file
|
||||||
[FileType.ShellScript, 'text/x-shellscript'], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
[FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||||
]);
|
};
|
||||||
|
|
||||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export interface ICommandOps {
|
|||||||
|
|
||||||
export interface IFileSystemOps {
|
export interface IFileSystemOps {
|
||||||
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
setFilePermissions(filePath: string, mode: string | number): Promise<void>;
|
||||||
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<string>;
|
createDirectory(directoryPath: string, isRecursive?: boolean): Promise<void>;
|
||||||
writeToFile(filePath: string, data: string): Promise<void>;
|
writeToFile(filePath: string, data: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,16 @@ export function createNodeSystemOperations(): ISystemOperations {
|
|||||||
filePath: string,
|
filePath: string,
|
||||||
mode: string | number,
|
mode: string | number,
|
||||||
) => chmod(filePath, mode),
|
) => chmod(filePath, mode),
|
||||||
createDirectory: (
|
createDirectory: async (
|
||||||
directoryPath: string,
|
directoryPath: string,
|
||||||
isRecursive?: boolean,
|
isRecursive?: boolean,
|
||||||
) => mkdir(directoryPath, { recursive: isRecursive }),
|
) => {
|
||||||
|
await mkdir(directoryPath, { recursive: isRecursive });
|
||||||
|
// Ignoring the return value from `mkdir`, which is the first directory created
|
||||||
|
// when `recursive` is true. The function contract is to not return any value,
|
||||||
|
// and we avoid handling this inconsistent behavior.
|
||||||
|
// See https://github.com/nodejs/node/pull/31530
|
||||||
|
},
|
||||||
writeToFile: (
|
writeToFile: (
|
||||||
filePath: string,
|
filePath: string,
|
||||||
data: string,
|
data: string,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { EventSource } from '../Events/EventSource';
|
import { EventSource } from '../Events/EventSource';
|
||||||
|
|
||||||
export class AsyncLazy<T> {
|
export class AsyncLazy<T> {
|
||||||
private valueCreated = new EventSource();
|
private valueCreated = new EventSource<T>();
|
||||||
|
|
||||||
private isValueCreated = false;
|
private state: ValueState<T> = { status: ValueStatus.NotRequested };
|
||||||
|
|
||||||
private isCreatingValue = false;
|
|
||||||
|
|
||||||
private value: T | undefined;
|
|
||||||
|
|
||||||
constructor(private valueFactory: () => Promise<T>) {}
|
constructor(private valueFactory: () => Promise<T>) {}
|
||||||
|
|
||||||
@@ -15,23 +11,44 @@ export class AsyncLazy<T> {
|
|||||||
this.valueFactory = valueFactory;
|
this.valueFactory = valueFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getValue(): Promise<T> {
|
public getValue(): Promise<T> {
|
||||||
// If value is already created, return the value directly
|
if (this.state.status === ValueStatus.Created) {
|
||||||
if (this.isValueCreated) {
|
return Promise.resolve(this.state.value);
|
||||||
return Promise.resolve(this.value);
|
|
||||||
}
|
}
|
||||||
// If value is being created, wait until the value is created and then return it.
|
if (this.state.status === ValueStatus.BeingCreated) {
|
||||||
if (this.isCreatingValue) {
|
return this.state.value;
|
||||||
return new Promise<T>((resolve) => {
|
|
||||||
// Return/result when valueCreated event is triggered.
|
|
||||||
this.valueCreated.on(() => resolve(this.value));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.isCreatingValue = true;
|
const valuePromise = this.valueFactory();
|
||||||
this.value = await this.valueFactory();
|
this.state = {
|
||||||
this.isCreatingValue = false;
|
status: ValueStatus.BeingCreated,
|
||||||
this.isValueCreated = true;
|
value: valuePromise,
|
||||||
this.valueCreated.notify(null);
|
};
|
||||||
return Promise.resolve(this.value);
|
valuePromise.then((value) => {
|
||||||
|
this.state = {
|
||||||
|
status: ValueStatus.Created,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
this.valueCreated.notify(value);
|
||||||
|
});
|
||||||
|
return valuePromise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ValueStatus {
|
||||||
|
NotRequested,
|
||||||
|
BeingCreated,
|
||||||
|
Created,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueState<T> =
|
||||||
|
| {
|
||||||
|
readonly status: ValueStatus.NotRequested;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly status: ValueStatus.BeingCreated;
|
||||||
|
readonly value: Promise<T>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly status: ValueStatus.Created;
|
||||||
|
readonly value: T
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { ILogger } from '@/infrastructure/Log/ILogger';
|
|||||||
|
|
||||||
/* Primary entry point for platform-specific injections */
|
/* Primary entry point for platform-specific injections */
|
||||||
export interface WindowVariables {
|
export interface WindowVariables {
|
||||||
readonly system: ISystemOperations;
|
readonly isDesktop?: boolean;
|
||||||
readonly isDesktop: boolean;
|
readonly system?: ISystemOperations;
|
||||||
readonly os: OperatingSystem;
|
readonly os?: OperatingSystem;
|
||||||
readonly log: ILogger;
|
readonly log?: ILogger;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
|
|||||||
|
|
||||||
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
|
||||||
const tests: {
|
const tests: {
|
||||||
[K in PropertyKeys<WindowVariables>]: boolean;
|
[K in PropertyKeys<Required<WindowVariables>>]: boolean;
|
||||||
} = {
|
} = {
|
||||||
os: testOperatingSystem(variables.os),
|
os: testOperatingSystem(variables.os),
|
||||||
isDesktop: testIsDesktop(variables.isDesktop),
|
isDesktop: testIsDesktop(variables.isDesktop),
|
||||||
|
|||||||
@@ -7,22 +7,18 @@
|
|||||||
<TheCodeButtons class="app__row app__code-buttons" />
|
<TheCodeButtons class="app__row app__code-buttons" />
|
||||||
<TheFooter />
|
<TheFooter />
|
||||||
</div>
|
</div>
|
||||||
<OptionalDevToolkit />
|
<component v-if="devToolkitComponent" :is="devToolkitComponent" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
import { defineAsyncComponent, defineComponent, Component } from 'vue';
|
||||||
import TheHeader from '@/presentation/components/TheHeader.vue';
|
import TheHeader from '@/presentation/components/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
||||||
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
|
|
||||||
const OptionalDevToolkit = process.env.NODE_ENV !== 'production'
|
|
||||||
? defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
TheHeader,
|
TheHeader,
|
||||||
@@ -30,10 +26,23 @@ export default defineComponent({
|
|||||||
TheScriptArea,
|
TheScriptArea,
|
||||||
TheSearchBar,
|
TheSearchBar,
|
||||||
TheFooter,
|
TheFooter,
|
||||||
OptionalDevToolkit,
|
|
||||||
},
|
},
|
||||||
setup() { },
|
setup() {
|
||||||
|
const devToolkitComponent = getOptionalDevToolkitComponent();
|
||||||
|
|
||||||
|
return {
|
||||||
|
devToolkitComponent,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getOptionalDevToolkitComponent(): Component | undefined {
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
if (!isDevelopment) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return defineAsyncComponent(() => import('@/presentation/components/DevToolkit/DevToolkit.vue'));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
|||||||
function getCanRunState(
|
function getCanRunState(
|
||||||
selectedOs: OperatingSystem,
|
selectedOs: OperatingSystem,
|
||||||
isDesktopVersion: boolean,
|
isDesktopVersion: boolean,
|
||||||
hostOs: OperatingSystem,
|
hostOs: OperatingSystem | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||||
return isDesktopVersion && isRunningOnSelectedOs;
|
return isDesktopVersion && isRunningOnSelectedOs;
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
|
|||||||
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import IconButton from '../IconButton.vue';
|
import IconButton from '../IconButton.vue';
|
||||||
import InstructionList from './Instructions/InstructionList.vue';
|
import InstructionList from './Instructions/InstructionList.vue';
|
||||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||||
import { getInstructions, hasInstructions } from './Instructions/InstructionListDataFactory';
|
import { getInstructions } from './Instructions/InstructionListDataFactory';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@@ -39,7 +38,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
const areInstructionsVisible = ref(false);
|
const areInstructionsVisible = ref(false);
|
||||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||||
const instructions = computed<IInstructionListData | undefined>(() => getDownloadInstructions(
|
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
|
||||||
currentState.value.collection.os,
|
currentState.value.collection.os,
|
||||||
fileName.value,
|
fileName.value,
|
||||||
));
|
));
|
||||||
@@ -59,16 +58,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDownloadInstructions(
|
|
||||||
os: OperatingSystem,
|
|
||||||
fileName: string,
|
|
||||||
): IInstructionListData | undefined {
|
|
||||||
if (!hasInstructions(os)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return getInstructions(os, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
function saveCodeToDisk(fileName: string, state: IReadOnlyCategoryCollectionState) {
|
||||||
const content = state.code.current;
|
const content = state.code.current;
|
||||||
const type = getType(state.collection.scripting.language);
|
const type = getType(state.collection.scripting.language);
|
||||||
|
|||||||
@@ -15,13 +15,11 @@ export class InstructionsBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public withStep(stepBuilder: InstructionStepBuilderType) {
|
public withStep(stepBuilder: InstructionStepBuilderType) {
|
||||||
if (!stepBuilder) { throw new Error('missing stepBuilder'); }
|
|
||||||
this.stepBuilders.push(stepBuilder);
|
this.stepBuilders.push(stepBuilder);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public build(data: IInstructionsBuilderData): IInstructionListData {
|
public build(data: IInstructionsBuilderData): IInstructionListData {
|
||||||
if (!data) { throw new Error('missing data'); }
|
|
||||||
return {
|
return {
|
||||||
operatingSystem: this.os,
|
operatingSystem: this.os,
|
||||||
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
|
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
|
||||||
|
|||||||
@@ -86,12 +86,9 @@ export default defineComponent({
|
|||||||
() => info.getDownloadUrl(OperatingSystem.macOS),
|
() => info.getDownloadUrl(OperatingSystem.macOS),
|
||||||
);
|
);
|
||||||
|
|
||||||
const osName = computed<string>(() => {
|
const osName = computed<string>(
|
||||||
if (!props.data) {
|
() => renderOsName(props.data.operatingSystem),
|
||||||
throw new Error('missing data');
|
);
|
||||||
}
|
|
||||||
return renderOsName(props.data.operatingSystem);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appName,
|
appName,
|
||||||
|
|||||||
@@ -9,15 +9,11 @@ const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
|||||||
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function hasInstructions(os: OperatingSystem) {
|
|
||||||
return builders.has(os);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInstructions(
|
export function getInstructions(
|
||||||
os: OperatingSystem,
|
os: OperatingSystem,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
): IInstructionListData {
|
): IInstructionListData | undefined {
|
||||||
return builders
|
return builders
|
||||||
.get(os)
|
.get(os)
|
||||||
.build({ fileName });
|
?.build({ fileName });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
function updateCode(code: string, language: ScriptingLanguage) {
|
function updateCode(code: string, language: ScriptingLanguage) {
|
||||||
const innerCode = code || getDefaultCode(language);
|
const innerCode = code || getDefaultCode(language);
|
||||||
editor.setValue(innerCode, 1);
|
editor?.setValue(innerCode, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCodeChange(event: ICodeChangedEvent) {
|
function handleCodeChange(event: ICodeChangedEvent) {
|
||||||
@@ -96,7 +96,7 @@ export default defineComponent({
|
|||||||
if (!currentMarkerId) {
|
if (!currentMarkerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
editor.session.removeMarker(currentMarkerId);
|
editor?.session.removeMarker(currentMarkerId);
|
||||||
currentMarkerId = undefined;
|
currentMarkerId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
function highlight(startRow: number, endRow: number) {
|
function highlight(startRow: number, endRow: number) {
|
||||||
const AceRange = ace.require('ace/range').Range;
|
const AceRange = ace.require('ace/range').Range;
|
||||||
currentMarkerId = editor.session.addMarker(
|
currentMarkerId = editor?.session.addMarker(
|
||||||
new AceRange(startRow, 0, endRow, 0),
|
new AceRange(startRow, 0, endRow, 0),
|
||||||
'code-area__highlight',
|
'code-area__highlight',
|
||||||
'fullLine',
|
'fullLine',
|
||||||
@@ -123,8 +123,11 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrollToLine(row: number) {
|
function scrollToLine(row: number) {
|
||||||
const column = editor.session.getLine(row).length;
|
const column = editor?.session.getLine(row).length;
|
||||||
editor.gotoLine(row, column, true);
|
if (column === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor?.gotoLine(row, column, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ export enum SelectionType {
|
|||||||
|
|
||||||
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
|
export function setCurrentSelectionType(type: SelectionType, context: SelectionMutationContext) {
|
||||||
if (type === SelectionType.Custom) {
|
if (type === SelectionType.Custom) {
|
||||||
throw new Error('cannot select custom type');
|
throw new Error('Cannot select custom type.');
|
||||||
}
|
}
|
||||||
const selector = selectors.get(type);
|
const selector = selectors.get(type);
|
||||||
|
if (!selector) {
|
||||||
|
throw new Error(`Cannot handle the type: ${SelectionType[type]}`);
|
||||||
|
}
|
||||||
selector.select(context);
|
selector.select(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export default defineComponent({
|
|||||||
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
|
const { modifyCurrentContext, currentState } = injectKey((keys) => keys.useCollectionState);
|
||||||
const { application } = injectKey((keys) => keys.useApplication);
|
const { application } = injectKey((keys) => keys.useApplication);
|
||||||
|
|
||||||
const allOses = computed<ReadonlyArray<IOsViewModel>>(() => (
|
const allOses = computed<ReadonlyArray<IOsViewModel>>(
|
||||||
application.getSupportedOsList() ?? [])
|
() => application
|
||||||
.map((os) : IOsViewModel => (
|
.getSupportedOsList()
|
||||||
{
|
.map((os) : IOsViewModel => ({
|
||||||
os,
|
os,
|
||||||
name: renderOsName(os),
|
name: renderOsName(os),
|
||||||
}
|
})),
|
||||||
)));
|
);
|
||||||
|
|
||||||
const currentOs = computed<OperatingSystem>(() => {
|
const currentOs = computed<OperatingSystem>(() => {
|
||||||
return currentState.value.os;
|
return currentState.value.os;
|
||||||
|
|||||||
@@ -48,8 +48,12 @@ export default defineComponent({
|
|||||||
const firstElement = shallowRef<HTMLElement>();
|
const firstElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
function onResize(displacementX: number): void {
|
function onResize(displacementX: number): void {
|
||||||
const leftWidth = firstElement.value.offsetWidth + displacementX;
|
const element = firstElement.value;
|
||||||
firstElement.value.style.width = `${leftWidth}px`;
|
if (!element) {
|
||||||
|
throw new Error('The element reference ref is not correctly assigned to a DOM element.');
|
||||||
|
}
|
||||||
|
const leftWidth = element.offsetWidth + displacementX;
|
||||||
|
element.style.width = `${leftWidth}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ export default defineComponent({
|
|||||||
const cursorCssValue = 'ew-resize';
|
const cursorCssValue = 'ew-resize';
|
||||||
let initialX: number | undefined;
|
let initialX: number | undefined;
|
||||||
|
|
||||||
const resize = (event) => {
|
const resize = (event: MouseEvent) => {
|
||||||
|
if (initialX === undefined) {
|
||||||
|
throw new Error('Resize action started without an initial X coordinate.');
|
||||||
|
}
|
||||||
const displacementX = event.clientX - initialX;
|
const displacementX = event.clientX - initialX;
|
||||||
emit('resized', displacementX);
|
emit('resized', displacementX);
|
||||||
initialX = event.clientX;
|
initialX = event.clientX;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<div
|
<div
|
||||||
v-if="categoryIds != null && categoryIds.length > 0"
|
v-if="categoryIds.length > 0"
|
||||||
class="cards"
|
class="cards"
|
||||||
>
|
>
|
||||||
<CardListItem
|
<CardListItem
|
||||||
@@ -50,8 +50,9 @@ export default defineComponent({
|
|||||||
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
const { currentState, onStateChange } = injectKey((keys) => keys.useCollectionState);
|
||||||
|
|
||||||
const width = ref<number>(0);
|
const width = ref<number>(0);
|
||||||
const categoryIds = computed<ReadonlyArray<number>>(() => currentState
|
const categoryIds = computed<readonly number[]>(
|
||||||
.value.collection.actions.map((category) => category.id));
|
() => currentState.value.collection.actions.map((category) => category.id),
|
||||||
|
);
|
||||||
const activeCategoryId = ref<number | undefined>(undefined);
|
const activeCategoryId = ref<number | undefined>(undefined);
|
||||||
|
|
||||||
function onSelected(categoryId: number, isExpanded: boolean) {
|
function onSelected(categoryId: number, isExpanded: boolean) {
|
||||||
|
|||||||
@@ -89,12 +89,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const cardElement = shallowRef<HTMLElement>();
|
const cardElement = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const cardTitle = computed<string | undefined>(() => {
|
const cardTitle = computed<string>(() => {
|
||||||
if (!props.categoryId || !currentState.value) {
|
const category = currentState.value.collection.getCategory(props.categoryId);
|
||||||
return undefined;
|
return category.name;
|
||||||
}
|
|
||||||
const category = currentState.value.collection.findCategory(props.categoryId);
|
|
||||||
return category?.name;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function collapse() {
|
function collapse() {
|
||||||
@@ -102,8 +99,12 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function scrollToCard() {
|
async function scrollToCard() {
|
||||||
|
const card = cardElement.value;
|
||||||
|
if (!card) {
|
||||||
|
throw new Error('Card is not found');
|
||||||
|
}
|
||||||
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
await sleep(400); // wait a bit to allow GUI to render the expanded card
|
||||||
cardElement.value.scrollIntoView({ behavior: 'smooth' });
|
card.scrollIntoView({ behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default defineComponent({
|
|||||||
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
const currentCollection = computed<ICategoryCollection>(() => currentState.value.collection);
|
||||||
|
|
||||||
const currentCategory = computed<ICategory>(
|
const currentCategory = computed<ICategory>(
|
||||||
() => currentCollection.value.findCategory(props.categoryId),
|
() => currentCollection.value.getCategory(props.categoryId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAnyChildSelected = computed<boolean>(
|
const isAnyChildSelected = computed<boolean>(
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export default defineComponent({
|
|||||||
const searchHasMatches = ref(false);
|
const searchHasMatches = ref(false);
|
||||||
const trimmedSearchQuery = computed(() => {
|
const trimmedSearchQuery = computed(() => {
|
||||||
const query = searchQuery.value;
|
const query = searchQuery.value;
|
||||||
|
if (!query) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const threshold = 30;
|
const threshold = 30;
|
||||||
if (query.length <= threshold - 3) {
|
if (query.length <= threshold - 3) {
|
||||||
return query;
|
return query;
|
||||||
@@ -94,7 +97,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
function updateFromInitialFilter(filter?: IFilterResult) {
|
function updateFromInitialFilter(filter?: IFilterResult) {
|
||||||
searchQuery.value = filter?.query;
|
searchQuery.value = filter?.query;
|
||||||
searchHasMatches.value = filter?.hasAnyMatches();
|
searchHasMatches.value = filter?.hasAnyMatches() ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
function subscribeToFilterChanges(filter: IReadOnlyUserFilter) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function renderText(docs: readonly string[] | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAsMarkdownListItem(content: string): string {
|
function renderAsMarkdownListItem(content: string): string {
|
||||||
if (!content || content.length === 0) {
|
if (content.length === 0) {
|
||||||
throw new Error('missing content');
|
throw new Error('missing content');
|
||||||
}
|
}
|
||||||
const lines = content.split(/\r\n|\r|\n/);
|
const lines = content.split(/\r\n|\r|\n/);
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ function removeTrailingExtension(value: string): string {
|
|||||||
if (parts.length === 1) {
|
if (parts.length === 1) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (parts.at(-1).length > 9) {
|
const extension = parts[parts.length - 1];
|
||||||
|
if (extension.length > 9) {
|
||||||
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
|
return value; // Heuristically web file extension is no longer than 9 chars (e.g. "html")
|
||||||
}
|
}
|
||||||
return parts.slice(0, -1).join('.');
|
return parts.slice(0, -1).join('.');
|
||||||
@@ -115,9 +116,8 @@ function selectMostDescriptiveName(parts: readonly string[]): string | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isGoodPathPart(part: string): boolean {
|
function isGoodPathPart(part: string): boolean {
|
||||||
return part
|
return part.length > 2 // This is often non-human categories like T5 etc.
|
||||||
&& !isDigit(part) // E.g. article numbers, issue numbers
|
&& !isDigit(part) // E.g. article numbers, issue numbers
|
||||||
&& part.length > 2 // This is often non-human categories like T5 etc.
|
|
||||||
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
|
&& !/^index(?:\.\S{0,10}$|$)/.test(part) // E.g. index.html
|
||||||
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
|
&& !/^[A-Za-z]{2,4}([_-][A-Za-z]{4})?([_-]([A-Za-z]{2}|[0-9]{3}))$/.test(part) // Locale string e.g. fr-FR
|
||||||
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID
|
&& !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(part) // GUID
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export interface NodeMetadata {
|
|||||||
readonly text: string;
|
readonly text: string;
|
||||||
readonly isReversible: boolean;
|
readonly isReversible: boolean;
|
||||||
readonly docs: ReadonlyArray<string>;
|
readonly docs: ReadonlyArray<string>;
|
||||||
readonly children?: ReadonlyArray<NodeMetadata>;
|
readonly children: ReadonlyArray<NodeMetadata>;
|
||||||
readonly type: NodeType;
|
readonly type: NodeType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ export class CategoryReverter implements IReverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
||||||
const category = collection.findCategory(categoryId);
|
const category = collection.getCategory(categoryId);
|
||||||
if (!category) {
|
|
||||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
|
||||||
}
|
|
||||||
const scripts = category.getAllScriptsRecursively();
|
const scripts = category.getAllScriptsRecursively();
|
||||||
return scripts.map((script) => new ScriptReverter(script.id));
|
return scripts.map((script) => new ScriptReverter(script.id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { useSelectedScriptNodeIds } from './TreeViewAdapter/UseSelectedScriptNod
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
categoryId: {
|
categoryId: {
|
||||||
type: [Number, undefined],
|
type: [Number],
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user