Refactor code to comply with ESLint rules
Major refactoring using ESLint with rules from AirBnb and Vue. Enable most of the ESLint rules and do necessary linting in the code. Also add more information for rules that are disabled to describe what they are and why they are disabled. Allow logging (`console.log`) in test files, and in development mode (e.g. when working with `npm run serve`), but disable it when environment is production (as pre-configured by Vue). Also add flag (`--mode production`) in `lint:eslint` command so production linting is executed earlier in lifecycle. Disable rules that requires a separate work. Such as ESLint rules that are broken in TypeScript: no-useless-constructor (eslint/eslint#14118) and no-shadow (eslint/eslint#13014).
This commit is contained in:
@@ -7,15 +7,18 @@ export type ApplicationGetter = () => IApplication;
|
||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
||||
|
||||
export class ApplicationFactory implements IApplicationFactory {
|
||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||
private readonly getter: AsyncLazy<IApplication>;
|
||||
protected constructor(costlyGetter: ApplicationGetter) {
|
||||
if (!costlyGetter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||
}
|
||||
public getApp(): Promise<IApplication> {
|
||||
return this.getter.getValue();
|
||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||
|
||||
private readonly getter: AsyncLazy<IApplication>;
|
||||
|
||||
protected constructor(costlyGetter: ApplicationGetter) {
|
||||
if (!costlyGetter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||
}
|
||||
|
||||
public getApp(): Promise<IApplication> {
|
||||
return this.getter.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
// Compares to Array<T> objects for equality, ignoring order
|
||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
if (!array1) { throw new Error('undefined first array'); }
|
||||
if (!array2) { throw new Error('undefined second array'); }
|
||||
const sortedArray1 = sort(array1);
|
||||
const sortedArray2 = sort(array2);
|
||||
return sequenceEqual(sortedArray1, sortedArray2);
|
||||
function sort(array: readonly T[]) {
|
||||
return array.slice().sort();
|
||||
}
|
||||
if (!array1) { throw new Error('undefined first array'); }
|
||||
if (!array2) { throw new Error('undefined second array'); }
|
||||
const sortedArray1 = sort(array1);
|
||||
const sortedArray2 = sort(array2);
|
||||
return sequenceEqual(sortedArray1, sortedArray2);
|
||||
function sort(array: readonly T[]) {
|
||||
return array.slice().sort();
|
||||
}
|
||||
}
|
||||
|
||||
// Compares to Array<T> objects for equality in same order
|
||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||
if (!array1) { throw new Error('undefined first array'); }
|
||||
if (!array2) { throw new Error('undefined second array'); }
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
return array1.every((val, index) => val === array2[index]);
|
||||
if (!array1) { throw new Error('undefined first array'); }
|
||||
if (!array2) { throw new Error('undefined second array'); }
|
||||
if (array1.length !== array2.length) {
|
||||
return false;
|
||||
}
|
||||
return array1.every((val, index) => val === array2[index]);
|
||||
}
|
||||
|
||||
@@ -1,54 +1,63 @@
|
||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||
export type EnumType = number | string;
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||
= { [key in T]: TEnumValue };
|
||||
|
||||
export interface IEnumParser<TEnum> {
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
}
|
||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: string,
|
||||
enumName: string,
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
||||
if (!value) {
|
||||
throw new Error(`undefined ${enumName}`);
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||
}
|
||||
const casedValue = getEnumNames(enumVariable)
|
||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||
if (!casedValue) {
|
||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||
}
|
||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
|
||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
}
|
||||
|
||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: string,
|
||||
enumName: string,
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): TEnumValue {
|
||||
if (!value) {
|
||||
throw new Error(`undefined ${enumName}`);
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||
}
|
||||
const casedValue = getEnumNames(enumVariable)
|
||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||
if (!casedValue) {
|
||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||
}
|
||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||
}
|
||||
|
||||
export function getEnumNames
|
||||
<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
}
|
||||
|
||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
||||
return getEnumNames(enumVariable)
|
||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): TEnumValue[] {
|
||||
return getEnumNames(enumVariable)
|
||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||
}
|
||||
|
||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: TEnumValue,
|
||||
enumVariable: EnumVariable<T, TEnumValue>) {
|
||||
if (value === undefined) {
|
||||
throw new Error('undefined enum value');
|
||||
}
|
||||
if (!(value in enumVariable)) {
|
||||
throw new RangeError(`enum value "${value}" is out of range`);
|
||||
}
|
||||
value: TEnumValue,
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
) {
|
||||
if (value === undefined) {
|
||||
throw new Error('undefined enum value');
|
||||
}
|
||||
if (!(value in enumVariable)) {
|
||||
throw new RangeError(`enum value "${value}" is out of range`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
|
||||
export interface IScriptingLanguageFactory<T> {
|
||||
create(language: ScriptingLanguage): T;
|
||||
create(language: ScriptingLanguage): T;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
||||
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||
|
||||
public create(language: ScriptingLanguage): T {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!this.getters.has(language)) {
|
||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||
}
|
||||
const getter = this.getters.get(language);
|
||||
const instance = getter();
|
||||
return instance;
|
||||
public create(language: ScriptingLanguage): T {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!this.getters.has(language)) {
|
||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||
}
|
||||
const getter = this.getters.get(language);
|
||||
const instance = getter();
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!getter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
if (this.getters.has(language)) {
|
||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||
}
|
||||
this.getters.set(language, getter);
|
||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||
assertInRange(language, ScriptingLanguage);
|
||||
if (!getter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
|
||||
if (this.getters.has(language)) {
|
||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||
}
|
||||
this.getters.set(language, getter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,64 @@
|
||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||
|
||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||
|
||||
export class ApplicationContext implements IApplicationContext {
|
||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||
public collection: ICategoryCollection;
|
||||
public currentOs: OperatingSystem;
|
||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||
|
||||
public get state(): ICategoryCollectionState {
|
||||
return this.states[this.collection.os];
|
||||
}
|
||||
public collection: ICategoryCollection;
|
||||
|
||||
private readonly states: StateMachine;
|
||||
public constructor(
|
||||
public readonly app: IApplication,
|
||||
initialContext: OperatingSystem) {
|
||||
validateApp(app);
|
||||
assertInRange(initialContext, OperatingSystem);
|
||||
this.states = initializeStates(app);
|
||||
this.changeContext(initialContext);
|
||||
}
|
||||
public currentOs: OperatingSystem;
|
||||
|
||||
public changeContext(os: OperatingSystem): void {
|
||||
if (this.currentOs === os) {
|
||||
return;
|
||||
}
|
||||
this.collection = this.app.getCollection(os);
|
||||
if (!this.collection) {
|
||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||
}
|
||||
const event: IApplicationContextChangedEvent = {
|
||||
newState: this.states[os],
|
||||
oldState: this.states[this.currentOs],
|
||||
};
|
||||
this.contextChanged.notify(event);
|
||||
this.currentOs = os;
|
||||
public get state(): ICategoryCollectionState {
|
||||
return this.states[this.collection.os];
|
||||
}
|
||||
|
||||
private readonly states: StateMachine;
|
||||
|
||||
public constructor(
|
||||
public readonly app: IApplication,
|
||||
initialContext: OperatingSystem,
|
||||
) {
|
||||
validateApp(app);
|
||||
assertInRange(initialContext, OperatingSystem);
|
||||
this.states = initializeStates(app);
|
||||
this.changeContext(initialContext);
|
||||
}
|
||||
|
||||
public changeContext(os: OperatingSystem): void {
|
||||
if (this.currentOs === os) {
|
||||
return;
|
||||
}
|
||||
this.collection = this.app.getCollection(os);
|
||||
if (!this.collection) {
|
||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||
}
|
||||
const event: IApplicationContextChangedEvent = {
|
||||
newState: this.states[os],
|
||||
oldState: this.states[this.currentOs],
|
||||
};
|
||||
this.contextChanged.notify(event);
|
||||
this.currentOs = os;
|
||||
}
|
||||
}
|
||||
|
||||
function validateApp(app: IApplication) {
|
||||
if (!app) {
|
||||
throw new Error('undefined app');
|
||||
}
|
||||
if (!app) {
|
||||
throw new Error('undefined app');
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStates(app: IApplication): StateMachine {
|
||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||
for (const collection of app.collections) {
|
||||
machine[collection.os] = new CategoryCollectionState(collection);
|
||||
}
|
||||
return machine;
|
||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||
for (const collection of app.collections) {
|
||||
machine[collection.os] = new CategoryCollectionState(collection);
|
||||
}
|
||||
return machine;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('undefined factory'); }
|
||||
if (!environment) { throw new Error('undefined environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment);
|
||||
return new ApplicationContext(app, os);
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('undefined factory'); }
|
||||
if (!environment) { throw new Error('undefined environment'); }
|
||||
const app = await factory.getApp();
|
||||
const os = getInitialOs(app, environment);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||
const currentOs = environment.os;
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
}
|
||||
supportedOsList.sort((os1, os2) => {
|
||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||
return getPriority(os2) - getPriority(os1);
|
||||
});
|
||||
return supportedOsList[0];
|
||||
const currentOs = environment.os;
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
}
|
||||
supportedOsList.sort((os1, os2) => {
|
||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||
return getPriority(os2) - getPriority(os1);
|
||||
});
|
||||
return supportedOsList[0];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
|
||||
export interface IReadOnlyApplicationContext {
|
||||
readonly app: IApplication;
|
||||
readonly state: IReadOnlyCategoryCollectionState;
|
||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||
readonly app: IApplication;
|
||||
readonly state: IReadOnlyCategoryCollectionState;
|
||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||
}
|
||||
|
||||
export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||
@@ -15,6 +15,6 @@ export interface IApplicationContext extends IReadOnlyApplicationContext {
|
||||
}
|
||||
|
||||
export interface IApplicationContextChangedEvent {
|
||||
readonly newState: ICategoryCollectionState;
|
||||
readonly oldState: ICategoryCollectionState;
|
||||
readonly newState: ICategoryCollectionState;
|
||||
readonly oldState: ICategoryCollectionState;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { UserFilter } from './Filter/UserFilter';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
@@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||
public readonly os: OperatingSystem;
|
||||
public readonly code: IApplicationCode;
|
||||
public readonly selection: IUserSelection;
|
||||
public readonly filter: IUserFilter;
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
public constructor(readonly collection: ICategoryCollection) {
|
||||
this.selection = new UserSelection(collection, []);
|
||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||
this.filter = new UserFilter(collection);
|
||||
this.os = collection.os;
|
||||
}
|
||||
public readonly code: IApplicationCode;
|
||||
|
||||
public readonly selection: IUserSelection;
|
||||
|
||||
public readonly filter: IUserFilter;
|
||||
|
||||
public constructor(readonly collection: ICategoryCollection) {
|
||||
this.selection = new UserSelection(collection, []);
|
||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||
this.filter = new UserFilter(collection);
|
||||
this.os = collection.os;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||
import { CodePosition } from './Position/CodePosition';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||
public current: string;
|
||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||
|
||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||
public current: string;
|
||||
|
||||
constructor(
|
||||
userSelection: IReadOnlyUserSelection,
|
||||
private readonly scriptingDefinition: IScriptingDefinition,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
this.setCode(scripts);
|
||||
});
|
||||
}
|
||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||
this.current = code.code;
|
||||
this.scriptPositions = code.scriptPositions;
|
||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||
this.changed.notify(event);
|
||||
}
|
||||
constructor(
|
||||
userSelection: IReadOnlyUserSelection,
|
||||
private readonly scriptingDefinition: IScriptingDefinition,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||
) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
this.setCode(scripts);
|
||||
});
|
||||
}
|
||||
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||
this.current = code.code;
|
||||
this.scriptPositions = code.scriptPositions;
|
||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||
this.changed.notify(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,72 @@
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
public readonly code: string;
|
||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||
public readonly code: string;
|
||||
|
||||
private readonly scripts: Map<IScript, ICodePosition>;
|
||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
constructor(
|
||||
code: string,
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
scripts: Map<SelectedScript, ICodePosition>) {
|
||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||
this.code = code;
|
||||
const newScripts = Array.from(scripts.keys());
|
||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||
this.scripts = new Map<IScript, ICodePosition>();
|
||||
scripts.forEach((position, selection) => {
|
||||
this.scripts.set(selection.script, position);
|
||||
});
|
||||
}
|
||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.scripts.size === 0;
|
||||
}
|
||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||
return this.scripts.get(script);
|
||||
}
|
||||
private readonly scripts: Map<IScript, ICodePosition>;
|
||||
|
||||
constructor(
|
||||
code: string,
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
scripts: Map<SelectedScript, ICodePosition>,
|
||||
) {
|
||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||
this.code = code;
|
||||
const newScripts = Array.from(scripts.keys());
|
||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||
this.scripts = new Map<IScript, ICodePosition>();
|
||||
scripts.forEach((position, selection) => {
|
||||
this.scripts.set(selection.script, position);
|
||||
});
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.scripts.size === 0;
|
||||
}
|
||||
|
||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||
return this.scripts.get(script);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||
for (const position of positions) {
|
||||
if (position.endLine > totalLines) {
|
||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||
`(total code lines: ${totalLines}`);
|
||||
}
|
||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||
for (const position of positions) {
|
||||
if (position.endLine > totalLines) {
|
||||
throw new Error(
|
||||
`script end line (${position.endLine}) is out of range.`
|
||||
+ `(total code lines: ${totalLines}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getChangedScripts(
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||
return newScripts
|
||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||
&& oldScript.revert !== newScript.revert ))
|
||||
.map((selection) => selection.script);
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
newScripts: ReadonlyArray<SelectedScript>,
|
||||
): ReadonlyArray<IScript> {
|
||||
return newScripts
|
||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||
&& oldScript.revert !== newScript.revert))
|
||||
.map((selection) => selection.script);
|
||||
}
|
||||
|
||||
function selectIfNotExists(
|
||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||
test: ReadonlyArray<SelectedScript>) {
|
||||
return selectableContainer
|
||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||
.map((selection) => selection.script);
|
||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||
test: ReadonlyArray<SelectedScript>,
|
||||
) {
|
||||
return selectableContainer
|
||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||
.map((selection) => selection.script);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface ICodeChangedEvent {
|
||||
readonly code: string;
|
||||
addedScripts: ReadonlyArray<IScript>;
|
||||
removedScripts: ReadonlyArray<IScript>;
|
||||
changedScripts: ReadonlyArray<IScript>;
|
||||
isEmpty(): boolean;
|
||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||
readonly code: string;
|
||||
addedScripts: ReadonlyArray<IScript>;
|
||||
removedScripts: ReadonlyArray<IScript>;
|
||||
changedScripts: ReadonlyArray<IScript>;
|
||||
isEmpty(): boolean;
|
||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||
}
|
||||
|
||||
@@ -4,64 +4,67 @@ const NewLine = '\n';
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
|
||||
export abstract class CodeBuilder implements ICodeBuilder {
|
||||
private readonly lines = new Array<string>();
|
||||
private readonly lines = new Array<string>();
|
||||
|
||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||
public get currentLine(): number {
|
||||
return this.lines.length;
|
||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||
public get currentLine(): number {
|
||||
return this.lines.length;
|
||||
}
|
||||
|
||||
public appendLine(code?: string): CodeBuilder {
|
||||
if (!code) {
|
||||
this.lines.push('');
|
||||
return this;
|
||||
}
|
||||
|
||||
public appendLine(code?: string): CodeBuilder {
|
||||
if (!code) {
|
||||
this.lines.push('');
|
||||
return this;
|
||||
}
|
||||
const lines = code.match(/[^\r\n]+/g);
|
||||
for (const line of lines) {
|
||||
this.lines.push(line);
|
||||
}
|
||||
return this;
|
||||
const lines = code.match(/[^\r\n]+/g);
|
||||
for (const line of lines) {
|
||||
this.lines.push(line);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public appendTrailingHyphensCommentLine(
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||
public appendTrailingHyphensCommentLine(
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||
): CodeBuilder {
|
||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||
}
|
||||
|
||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||
return this;
|
||||
}
|
||||
|
||||
public appendFunction(name: string, code: string): CodeBuilder {
|
||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||
return this
|
||||
.appendCommentLineWithHyphensAround(name)
|
||||
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||
.appendLine(code)
|
||||
.appendTrailingHyphensCommentLine();
|
||||
}
|
||||
|
||||
public appendCommentLineWithHyphensAround(
|
||||
sectionName: string,
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||
): CodeBuilder {
|
||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||
if (sectionName.length >= totalRepeatHyphens) {
|
||||
return this.appendCommentLine(sectionName);
|
||||
}
|
||||
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
|
||||
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
|
||||
return this
|
||||
.appendTrailingHyphensCommentLine()
|
||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||
}
|
||||
|
||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||
return this;
|
||||
}
|
||||
public toString(): string {
|
||||
return this.lines.join(NewLine);
|
||||
}
|
||||
|
||||
public appendFunction(name: string, code: string): CodeBuilder {
|
||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||
return this
|
||||
.appendCommentLineWithHyphensAround(name)
|
||||
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||
.appendLine(code)
|
||||
.appendTrailingHyphensCommentLine();
|
||||
}
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
|
||||
public appendCommentLineWithHyphensAround(
|
||||
sectionName: string,
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||
if (sectionName.length >= totalRepeatHyphens) {
|
||||
return this.appendCommentLine(sectionName);
|
||||
}
|
||||
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
|
||||
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
|
||||
return this
|
||||
.appendTrailingHyphensCommentLine()
|
||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.lines.join(NewLine);
|
||||
}
|
||||
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import { BatchBuilder } from './Languages/BatchBuilder';
|
||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
|
||||
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||
}
|
||||
export class CodeBuilderFactory
|
||||
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||
implements ICodeBuilderFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface ICodeBuilder {
|
||||
currentLine: number;
|
||||
appendLine(code?: string): ICodeBuilder;
|
||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendFunction(name: string, code: string): ICodeBuilder;
|
||||
toString(): string;
|
||||
currentLine: number;
|
||||
appendLine(code?: string): ICodeBuilder;
|
||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendFunction(name: string, code: string): ICodeBuilder;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
|
||||
}
|
||||
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||
|
||||
@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface IUserScript {
|
||||
code: string;
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||
code: string;
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||
|
||||
export class BatchBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '::';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
protected getCommentDelimiter(): string {
|
||||
return '::';
|
||||
}
|
||||
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '^&')
|
||||
.replace(/%/g, '%%');
|
||||
return text
|
||||
.replace(/&/g, '^&')
|
||||
.replace(/%/g, '%%');
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||
|
||||
export class ShellBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '#';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
protected getCommentDelimiter(): string {
|
||||
return '#';
|
||||
}
|
||||
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
return text
|
||||
.replace(/'/g, '\'\\\'\'');
|
||||
return text
|
||||
.replace(/'/g, '\'\\\'\'');
|
||||
}
|
||||
|
||||
@@ -1,71 +1,76 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||
|
||||
}
|
||||
|
||||
public buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition,
|
||||
): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
public buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||
return { code, scriptPositions };
|
||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||
return { code, scriptPositions };
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||
if (!startCode) {
|
||||
return builder;
|
||||
}
|
||||
return builder
|
||||
.appendLine(startCode)
|
||||
.appendLine();
|
||||
if (!startCode) {
|
||||
return builder;
|
||||
}
|
||||
return builder
|
||||
.appendLine(startCode)
|
||||
.appendLine();
|
||||
}
|
||||
|
||||
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||
if (!endCode) {
|
||||
return builder.toString();
|
||||
}
|
||||
return builder.appendLine()
|
||||
.appendLine(endCode)
|
||||
.toString();
|
||||
if (!endCode) {
|
||||
return builder.toString();
|
||||
}
|
||||
return builder.appendLine()
|
||||
.appendLine(endCode)
|
||||
.toString();
|
||||
}
|
||||
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
||||
builder = appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
const position = new CodePosition(startPosition, endPosition);
|
||||
scriptPositions.set(selection, position);
|
||||
return scriptPositions;
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: ICodeBuilder,
|
||||
): Map<SelectedScript, ICodePosition> {
|
||||
// Start from next line because first line will be empty to separate scripts
|
||||
const startPosition = builder.currentLine + 1;
|
||||
appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
const position = new CodePosition(startPosition, endPosition);
|
||||
scriptPositions.set(selection, position);
|
||||
return scriptPositions;
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||
return builder
|
||||
.appendLine()
|
||||
.appendFunction(name, scriptCode);
|
||||
const { script } = selection;
|
||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||
return builder
|
||||
.appendLine()
|
||||
.appendFunction(name, scriptCode);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
|
||||
export interface IApplicationCode {
|
||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||
readonly current: string;
|
||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||
readonly current: string;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { ICodePosition } from './ICodePosition';
|
||||
|
||||
export class CodePosition implements ICodePosition {
|
||||
public get totalLines(): number {
|
||||
return this.endLine - this.startLine;
|
||||
}
|
||||
public get totalLines(): number {
|
||||
return this.endLine - this.startLine;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly startLine: number,
|
||||
public readonly endLine: number) {
|
||||
if (startLine < 0) {
|
||||
throw new Error('Code cannot start in a negative line');
|
||||
}
|
||||
if (endLine < 0) {
|
||||
throw new Error('Code cannot end in a negative line');
|
||||
}
|
||||
if (endLine === startLine) {
|
||||
throw new Error('Empty code');
|
||||
}
|
||||
if (endLine < startLine) {
|
||||
throw new Error('End line cannot be less than start line');
|
||||
}
|
||||
constructor(
|
||||
public readonly startLine: number,
|
||||
public readonly endLine: number,
|
||||
) {
|
||||
if (startLine < 0) {
|
||||
throw new Error('Code cannot start in a negative line');
|
||||
}
|
||||
if (endLine < 0) {
|
||||
throw new Error('Code cannot end in a negative line');
|
||||
}
|
||||
if (endLine === startLine) {
|
||||
throw new Error('Empty code');
|
||||
}
|
||||
if (endLine < startLine) {
|
||||
throw new Error('End line cannot be less than start line');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface ICodePosition {
|
||||
readonly startLine: number;
|
||||
readonly endLine: number;
|
||||
readonly totalLines: number;
|
||||
readonly startLine: number;
|
||||
readonly endLine: number;
|
||||
readonly totalLines: number;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
|
||||
export class FilterResult implements IFilterResult {
|
||||
constructor(
|
||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||
public readonly query: string) {
|
||||
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 {
|
||||
return this.scriptMatches.length > 0
|
||||
|| this.categoryMatches.length > 0;
|
||||
}
|
||||
constructor(
|
||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||
public readonly query: string,
|
||||
) {
|
||||
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 {
|
||||
return this.scriptMatches.length > 0
|
||||
|| this.categoryMatches.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IScript, ICategory } from '@/domain/ICategory';
|
||||
|
||||
export interface IFilterResult {
|
||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||
readonly query: string;
|
||||
hasAnyMatches(): boolean;
|
||||
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||
readonly query: string;
|
||||
hasAnyMatches(): boolean;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
|
||||
export interface IReadOnlyUserFilter {
|
||||
readonly currentFilter: IFilterResult | undefined;
|
||||
readonly filtered: IEventSource<IFilterResult>;
|
||||
readonly filterRemoved: IEventSource<void>;
|
||||
readonly currentFilter: IFilterResult | undefined;
|
||||
readonly filtered: IEventSource<IFilterResult>;
|
||||
readonly filterRemoved: IEventSource<void>;
|
||||
}
|
||||
|
||||
export interface IUserFilter extends IReadOnlyUserFilter {
|
||||
setFilter(filter: string): void;
|
||||
removeFilter(): void;
|
||||
setFilter(filter: string): void;
|
||||
removeFilter(): void;
|
||||
}
|
||||
|
||||
@@ -1,52 +1,56 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new EventSource<IFilterResult>();
|
||||
public readonly filterRemoved = new EventSource<void>();
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
public readonly filtered = new EventSource<IFilterResult>();
|
||||
|
||||
constructor(private collection: ICategoryCollection) {
|
||||
public readonly filterRemoved = new EventSource<void>();
|
||||
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
|
||||
constructor(private collection: ICategoryCollection) {
|
||||
|
||||
}
|
||||
|
||||
public setFilter(filter: string): void {
|
||||
if (!filter) {
|
||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.collection.getAllScripts().filter(
|
||||
(script) => isScriptAMatch(script, filterLowercase),
|
||||
);
|
||||
const filteredCategories = this.collection.getAllCategories().filter(
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase),
|
||||
);
|
||||
const matches = new FilterResult(
|
||||
filteredScripts,
|
||||
filteredCategories,
|
||||
filter,
|
||||
);
|
||||
this.currentFilter = matches;
|
||||
this.filtered.notify(matches);
|
||||
}
|
||||
|
||||
public setFilter(filter: string): void {
|
||||
if (!filter) {
|
||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.collection.getAllScripts().filter(
|
||||
(script) => isScriptAMatch(script, filterLowercase));
|
||||
const filteredCategories = this.collection.getAllCategories().filter(
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||
const matches = new FilterResult(
|
||||
filteredScripts,
|
||||
filteredCategories,
|
||||
filter,
|
||||
);
|
||||
this.currentFilter = matches;
|
||||
this.filtered.notify(matches);
|
||||
}
|
||||
|
||||
public removeFilter(): void {
|
||||
this.currentFilter = undefined;
|
||||
this.filterRemoved.notify();
|
||||
}
|
||||
public removeFilter(): void {
|
||||
this.currentFilter = undefined;
|
||||
this.filterRemoved.notify();
|
||||
}
|
||||
}
|
||||
|
||||
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.code.revert) {
|
||||
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||
}
|
||||
return false;
|
||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||
return true;
|
||||
}
|
||||
if (script.code.revert) {
|
||||
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IReadOnlyCategoryCollectionState {
|
||||
readonly code: IApplicationCode;
|
||||
readonly os: OperatingSystem;
|
||||
readonly filter: IReadOnlyUserFilter;
|
||||
readonly selection: IReadOnlyUserSelection;
|
||||
readonly collection: ICategoryCollection;
|
||||
readonly code: IApplicationCode;
|
||||
readonly os: OperatingSystem;
|
||||
readonly filter: IReadOnlyUserFilter;
|
||||
readonly selection: IReadOnlyUserSelection;
|
||||
readonly collection: ICategoryCollection;
|
||||
}
|
||||
|
||||
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||
readonly filter: IUserFilter;
|
||||
readonly selection: IUserSelection;
|
||||
readonly filter: IUserFilter;
|
||||
readonly selection: IUserSelection;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export interface IReadOnlyUserSelection {
|
||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
isSelected(scriptId: string): boolean;
|
||||
areAllSelected(category: ICategory): boolean;
|
||||
isAnySelected(category: ICategory): boolean;
|
||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
isSelected(scriptId: string): boolean;
|
||||
areAllSelected(category: ICategory): boolean;
|
||||
isAnySelected(category: ICategory): boolean;
|
||||
}
|
||||
|
||||
export interface IUserSelection extends IReadOnlyUserSelection {
|
||||
removeAllInCategory(categoryId: number): void;
|
||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||
removeSelectedScript(scriptId: string): void;
|
||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
removeAllInCategory(categoryId: number): void;
|
||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||
removeSelectedScript(scriptId: string): void;
|
||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||
selectAll(): void;
|
||||
deselectAll(): void;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
|
||||
export class SelectedScript extends BaseEntity<string> {
|
||||
constructor(
|
||||
public readonly script: IScript,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error('cannot revert an irreversible script');
|
||||
}
|
||||
constructor(
|
||||
public readonly script: IScript,
|
||||
public readonly revert: boolean,
|
||||
) {
|
||||
super(script.id);
|
||||
if (revert && !script.canRevert()) {
|
||||
throw new Error('cannot revert an irreversible script');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,145 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
constructor(
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
public areAllSelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
if (this.selectedScripts.length < scripts.length) {
|
||||
return false;
|
||||
}
|
||||
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
||||
constructor(
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
for (const script of selectedScripts) {
|
||||
this.scripts.addItem(script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public isAnySelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||
public areAllSelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const scripts = category.getAllScriptsRecursively();
|
||||
if (this.selectedScripts.length < scripts.length) {
|
||||
return false;
|
||||
}
|
||||
return scripts.every(
|
||||
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
||||
);
|
||||
}
|
||||
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToRemove) {
|
||||
this.scripts.removeItem(script.id);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
public isAnySelected(category: ICategory): boolean {
|
||||
if (this.selectedScripts.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||
}
|
||||
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||
.filter((script) =>
|
||||
!this.scripts.exists(script.id)
|
||||
|| this.scripts.getById(script.id).revert !== revert,
|
||||
);
|
||||
if (!scriptsToAddOrUpdate.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToAddOrUpdate) {
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToRemove) {
|
||||
this.scripts.removeItem(script.id);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||
}
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||
.filter(
|
||||
(script) => !this.scripts.exists(script.id)
|
||||
|| this.scripts.getById(script.id).revert !== revert,
|
||||
);
|
||||
if (!scriptsToAddOrUpdate.length) {
|
||||
return;
|
||||
}
|
||||
for (const script of scriptsToAddOrUpdate) {
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||
}
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public removeSelectedScript(scriptId: string): void {
|
||||
this.scripts.removeItem(scriptId);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.collection.findScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
}
|
||||
public removeSelectedScript(scriptId: string): void {
|
||||
this.scripts.removeItem(scriptId);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
/** Get users scripts based on his/her selections */
|
||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||
return this.scripts.getItems();
|
||||
}
|
||||
public isSelected(scriptId: string): boolean {
|
||||
return this.scripts.exists(scriptId);
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
for (const script of this.collection.getAllScripts()) {
|
||||
if (!this.scripts.exists(script.id)) {
|
||||
const selection = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
/** Get users scripts based on his/her selections */
|
||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||
return this.scripts.getItems();
|
||||
}
|
||||
|
||||
public deselectAll(): void {
|
||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||
for (const scriptId of selectedScriptIds) {
|
||||
this.scripts.removeItem(scriptId);
|
||||
}
|
||||
this.changed.notify([]);
|
||||
public selectAll(): void {
|
||||
for (const script of this.collection.getAllScripts()) {
|
||||
if (!this.scripts.exists(script.id)) {
|
||||
const selection = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
if (!scripts || scripts.length === 0) {
|
||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||
}
|
||||
// Unselect from selected scripts
|
||||
if (this.scripts.length !== 0) {
|
||||
this.scripts.getItems()
|
||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
||||
.map((script) => script.id)
|
||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||
}
|
||||
// Select from unselected scripts
|
||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
||||
for (const toSelect of unselectedScripts) {
|
||||
const selection = new SelectedScript(toSelect, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
public deselectAll(): void {
|
||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||
for (const scriptId of selectedScriptIds) {
|
||||
this.scripts.removeItem(scriptId);
|
||||
}
|
||||
this.changed.notify([]);
|
||||
}
|
||||
|
||||
public selectOnly(scripts: readonly IScript[]): void {
|
||||
if (!scripts || scripts.length === 0) {
|
||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
||||
}
|
||||
// Unselect from selected scripts
|
||||
if (this.scripts.length !== 0) {
|
||||
this.scripts.getItems()
|
||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
||||
.map((script) => script.id)
|
||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||
}
|
||||
// Select from unselected scripts
|
||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
||||
for (const toSelect of unselectedScripts) {
|
||||
const selection = new SelectedScript(toSelect, false);
|
||||
this.scripts.addItem(selection);
|
||||
}
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,52 +3,55 @@ import { DetectorBuilder } from './DetectorBuilder';
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||
private readonly detectors = BrowserDetectors;
|
||||
public detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
for (const detector of this.detectors) {
|
||||
const os = detector.detect(userAgent);
|
||||
if (os !== undefined) {
|
||||
return os;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
private readonly detectors = BrowserDetectors;
|
||||
|
||||
public detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
}
|
||||
for (const detector of this.detectors) {
|
||||
const os = detector.detect(userAgent);
|
||||
if (os !== undefined) {
|
||||
return os;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||
const BrowserDetectors =
|
||||
[
|
||||
define(OperatingSystem.KaiOS, (b) =>
|
||||
b.mustInclude('KAIOS')),
|
||||
define(OperatingSystem.ChromeOS, (b) =>
|
||||
b.mustInclude('CrOS')),
|
||||
define(OperatingSystem.BlackBerryOS, (b) =>
|
||||
b.mustInclude('BlackBerry')),
|
||||
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
||||
b.mustInclude('RIM Tablet OS')),
|
||||
define(OperatingSystem.BlackBerry, (b) =>
|
||||
b.mustInclude('BB10')),
|
||||
define(OperatingSystem.Android, (b) =>
|
||||
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.Android, (b) =>
|
||||
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.iOS, (b) =>
|
||||
b.mustInclude('like Mac OS X')),
|
||||
define(OperatingSystem.Linux, (b) =>
|
||||
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||
define(OperatingSystem.Windows, (b) =>
|
||||
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.WindowsPhone, (b) =>
|
||||
b.mustInclude('Windows Phone')),
|
||||
define(OperatingSystem.macOS, (b) =>
|
||||
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||
const BrowserDetectors = [
|
||||
define(OperatingSystem.KaiOS, (b) => b
|
||||
.mustInclude('KAIOS')),
|
||||
define(OperatingSystem.ChromeOS, (b) => b
|
||||
.mustInclude('CrOS')),
|
||||
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||
.mustInclude('BlackBerry')),
|
||||
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||
.mustInclude('RIM Tablet OS')),
|
||||
define(OperatingSystem.BlackBerry, (b) => b
|
||||
.mustInclude('BB10')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.iOS, (b) => b
|
||||
.mustInclude('like Mac OS X')),
|
||||
define(OperatingSystem.Linux, (b) => b
|
||||
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||
define(OperatingSystem.Windows, (b) => b
|
||||
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.WindowsPhone, (b) => b
|
||||
.mustInclude('Windows Phone')),
|
||||
define(OperatingSystem.macOS, (b) => b
|
||||
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||
];
|
||||
|
||||
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
||||
const builder = new DetectorBuilder(os);
|
||||
applyRules(builder);
|
||||
return builder.build();
|
||||
function define(
|
||||
os: OperatingSystem,
|
||||
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||
): IBrowserOsDetector {
|
||||
const builder = new DetectorBuilder(os);
|
||||
applyRules(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class DetectorBuilder {
|
||||
private readonly existingPartsInUserAgent = new Array<string>();
|
||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||
private readonly existingPartsInUserAgent = new Array<string>();
|
||||
|
||||
constructor(private readonly os: OperatingSystem) { }
|
||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||
|
||||
public mustInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.existingPartsInUserAgent);
|
||||
constructor(private readonly os: OperatingSystem) { }
|
||||
|
||||
public mustInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.existingPartsInUserAgent);
|
||||
}
|
||||
|
||||
public mustNotInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.notExistingPartsInUserAgent);
|
||||
}
|
||||
|
||||
public build(): IBrowserOsDetector {
|
||||
if (!this.existingPartsInUserAgent.length) {
|
||||
throw new Error('Must include at least a part');
|
||||
}
|
||||
return {
|
||||
detect: (agent) => this.detect(agent),
|
||||
};
|
||||
}
|
||||
|
||||
public mustNotInclude(str: string): DetectorBuilder {
|
||||
return this.add(str, this.notExistingPartsInUserAgent);
|
||||
private detect(userAgent: string): OperatingSystem {
|
||||
if (!userAgent) {
|
||||
throw new Error('User agent is null or undefined');
|
||||
}
|
||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
return this.os;
|
||||
}
|
||||
|
||||
public build(): IBrowserOsDetector {
|
||||
if (!this.existingPartsInUserAgent.length) {
|
||||
throw new Error('Must include at least a part');
|
||||
}
|
||||
return {
|
||||
detect: (agent) => this.detect(agent),
|
||||
};
|
||||
private add(part: string, array: string[]): DetectorBuilder {
|
||||
if (!part) {
|
||||
throw new Error('part is empty or undefined');
|
||||
}
|
||||
|
||||
private detect(userAgent: string): OperatingSystem {
|
||||
if (!userAgent) {
|
||||
throw new Error('User agent is null or undefined');
|
||||
}
|
||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||
return undefined;
|
||||
}
|
||||
return this.os;
|
||||
if (this.existingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as existing part`);
|
||||
}
|
||||
|
||||
private add(part: string, array: string[]): DetectorBuilder {
|
||||
if (!part) {
|
||||
throw new Error('part is empty or undefined');
|
||||
}
|
||||
if (this.existingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as existing part`);
|
||||
}
|
||||
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as not existing part`);
|
||||
}
|
||||
array.push(part);
|
||||
return this;
|
||||
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||
throw new Error(`part ${part} is already included as not existing part`);
|
||||
}
|
||||
array.push(part);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IBrowserOsDetector {
|
||||
detect(userAgent: string): OperatingSystem | undefined;
|
||||
detect(userAgent: string): OperatingSystem | undefined;
|
||||
}
|
||||
|
||||
@@ -1,83 +1,89 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IEnvironment } from './IEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
interface IEnvironmentVariables {
|
||||
readonly window: Window & typeof globalThis;
|
||||
readonly process: NodeJS.Process;
|
||||
readonly navigator: Navigator;
|
||||
export interface IEnvironmentVariables {
|
||||
readonly window: Window & typeof globalThis;
|
||||
readonly process: NodeJS.Process;
|
||||
readonly navigator: Navigator;
|
||||
}
|
||||
|
||||
export class Environment implements IEnvironment {
|
||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||
window,
|
||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||
navigator,
|
||||
});
|
||||
public readonly isDesktop: boolean;
|
||||
public readonly os: OperatingSystem;
|
||||
protected constructor(
|
||||
variables: IEnvironmentVariables,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
||||
if (!variables) {
|
||||
throw new Error('variables is null or empty');
|
||||
}
|
||||
this.isDesktop = isDesktop(variables);
|
||||
if (this.isDesktop) {
|
||||
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||
} else {
|
||||
const userAgent = getUserAgent(variables);
|
||||
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||
}
|
||||
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||
window,
|
||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||
navigator,
|
||||
});
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
protected constructor(
|
||||
variables: IEnvironmentVariables,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
) {
|
||||
if (!variables) {
|
||||
throw new Error('variables is null or empty');
|
||||
}
|
||||
this.isDesktop = isDesktop(variables);
|
||||
if (this.isDesktop) {
|
||||
this.os = getDesktopOsType(getProcessPlatform(variables));
|
||||
} else {
|
||||
const userAgent = getUserAgent(variables);
|
||||
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||
if (!variables.window || !variables.window.navigator) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.window.navigator.userAgent;
|
||||
if (!variables.window || !variables.window.navigator) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.window.navigator.userAgent;
|
||||
}
|
||||
|
||||
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||
if (!variables.process || !variables.process.platform) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.process.platform;
|
||||
if (!variables.process || !variables.process.platform) {
|
||||
return undefined;
|
||||
}
|
||||
return variables.process.platform;
|
||||
}
|
||||
|
||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||
// https://nodejs.org/api/process.html#process_process_platform
|
||||
if (processPlatform === 'darwin') {
|
||||
return OperatingSystem.macOS;
|
||||
} else if (processPlatform === 'win32') {
|
||||
return OperatingSystem.Windows;
|
||||
} else if (processPlatform === 'linux') {
|
||||
return OperatingSystem.Linux;
|
||||
}
|
||||
return undefined;
|
||||
// https://nodejs.org/api/process.html#process_process_platform
|
||||
switch (processPlatform) {
|
||||
case 'darwin':
|
||||
return OperatingSystem.macOS;
|
||||
case 'win32':
|
||||
return OperatingSystem.Windows;
|
||||
case 'linux':
|
||||
return OperatingSystem.Linux;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||
// More: https://github.com/electron/electron/issues/2288
|
||||
// Renderer process
|
||||
if (variables.window
|
||||
&& variables.window.process
|
||||
&& variables.window.process.type === 'renderer') {
|
||||
return true;
|
||||
}
|
||||
// Main process
|
||||
if (variables.process
|
||||
&& variables.process.versions
|
||||
&& Boolean(variables.process.versions.electron)) {
|
||||
return true;
|
||||
}
|
||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||
if (variables.navigator
|
||||
&& variables.navigator.userAgent
|
||||
&& variables.navigator.userAgent.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
// More: https://github.com/electron/electron/issues/2288
|
||||
// Renderer process
|
||||
if (variables.window
|
||||
&& variables.window.process
|
||||
&& variables.window.process.type === 'renderer') {
|
||||
return true;
|
||||
}
|
||||
// Main process
|
||||
if (variables.process
|
||||
&& variables.process.versions
|
||||
&& Boolean(variables.process.versions.electron)) {
|
||||
return true;
|
||||
}
|
||||
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||
if (variables.navigator
|
||||
&& variables.navigator.userAgent
|
||||
&& variables.navigator.userAgent.includes('Electron')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export interface IApplicationFactory {
|
||||
getApp(): Promise<IApplication>;
|
||||
getApp(): Promise<IApplication>;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
collectionsData = PreParsedCollections): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
}
|
||||
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||
|
||||
const CategoryCollectionParser: CategoryCollectionParserType
|
||||
= (file, info) => parseCategoryCollection(file, info);
|
||||
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||
return parseCategoryCollection(file, info);
|
||||
};
|
||||
|
||||
const PreParsedCollections: readonly CollectionData []
|
||||
= [ WindowsData, MacOsData ];
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections.length) {
|
||||
throw new Error('no collection provided');
|
||||
}
|
||||
if (collections.some((collection) => !collection)) {
|
||||
throw new Error('undefined collection provided');
|
||||
}
|
||||
if (!collections.length) {
|
||||
throw new Error('no collection provided');
|
||||
}
|
||||
if (collections.some((collection) => !collection)) {
|
||||
throw new Error('undefined collection provided');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import { Category } from '@/domain/Category';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
info: IProjectInformation,
|
||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = new ScriptingDefinitionParser()
|
||||
.parse(content.scripting, info);
|
||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||
const categories = new Array<Category>();
|
||||
for (const action of content.actions) {
|
||||
const category = parseCategory(action, context);
|
||||
categories.push(category);
|
||||
}
|
||||
const os = osParser.parseEnum(content.os, 'os');
|
||||
const collection = new CategoryCollection(
|
||||
os,
|
||||
categories,
|
||||
scripting);
|
||||
return collection;
|
||||
content: CollectionData,
|
||||
info: IProjectInformation,
|
||||
osParser = createEnumParser(OperatingSystem),
|
||||
): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = new ScriptingDefinitionParser()
|
||||
.parse(content.scripting, info);
|
||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||
const categories = new Array<Category>();
|
||||
for (const action of content.actions) {
|
||||
const category = parseCategory(action, context);
|
||||
categories.push(category);
|
||||
}
|
||||
const os = osParser.parseEnum(content.os, 'os');
|
||||
const collection = new CategoryCollection(
|
||||
os,
|
||||
categories,
|
||||
scripting,
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
|
||||
function validate(content: CollectionData): void {
|
||||
if (!content) {
|
||||
throw new Error('content is null or undefined');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error('content is null or undefined');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,86 @@
|
||||
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
||||
import {
|
||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
||||
} from 'js-yaml-loader!@/*';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
|
||||
let categoryIdCounter: number = 0;
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
interface ICategoryChildren {
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
ensureValid(category);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of category.children) {
|
||||
parseCategoryChild(data, children, category, context);
|
||||
}
|
||||
return new Category(
|
||||
/*id*/ categoryIdCounter++,
|
||||
/*name*/ category.category,
|
||||
/*docs*/ parseDocUrls(category),
|
||||
/*categories*/ children.subCategories,
|
||||
/*scripts*/ children.subScripts,
|
||||
);
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
): Category {
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
ensureValid(category);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of category.children) {
|
||||
parseCategoryChild(data, children, category, context);
|
||||
}
|
||||
return new Category(
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ category.category,
|
||||
/* docs: */ parseDocUrls(category),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
}
|
||||
|
||||
function ensureValid(category: CategoryData) {
|
||||
if (!category) {
|
||||
throw Error('category is null or undefined');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error(`category has no children: "${category.category}"`);
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
}
|
||||
if (!category) {
|
||||
throw Error('category is null or undefined');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error(`category has no children: "${category.category}"`);
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
}
|
||||
}
|
||||
|
||||
function parseCategoryChild(
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(data)) {
|
||||
const scriptData = data as ScriptData;
|
||||
const script = parseScript(scriptData, context);
|
||||
children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(data)) {
|
||||
const scriptData = data as ScriptData;
|
||||
const script = parseScript(scriptData, context);
|
||||
children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isScript(data: any): boolean {
|
||||
return (data.code && data.code.length > 0)
|
||||
|| data.call;
|
||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
const holder = (data as InstructionHolder);
|
||||
return hasCode(holder) || hasCall(holder);
|
||||
}
|
||||
|
||||
function isCategory(data: any): boolean {
|
||||
return data.category && data.category.length > 0;
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
const { category } = data as CategoryData;
|
||||
return category && category.length > 0;
|
||||
}
|
||||
|
||||
function hasCode(holder: InstructionHolder): boolean {
|
||||
return holder.code && holder.code.length > 0;
|
||||
}
|
||||
|
||||
function hasCall(holder: InstructionHolder) {
|
||||
return holder.call !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,61 +1,64 @@
|
||||
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
||||
|
||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||
if (!documentable) {
|
||||
throw new Error('documentable is null or undefined');
|
||||
}
|
||||
const docs = documentable.docs;
|
||||
if (!docs || !docs.length) {
|
||||
return [];
|
||||
}
|
||||
let result = new DocumentationUrlContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
if (!documentable) {
|
||||
throw new Error('documentable is null or undefined');
|
||||
}
|
||||
const { docs } = documentable;
|
||||
if (!docs || !docs.length) {
|
||||
return [];
|
||||
}
|
||||
let result = new DocumentationUrlContainer();
|
||||
result = addDocs(docs, result);
|
||||
return result.getAll();
|
||||
}
|
||||
|
||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
} else if (typeof docs === 'string') {
|
||||
urls.addUrl(docs);
|
||||
} else {
|
||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||
}
|
||||
return urls;
|
||||
function addDocs(
|
||||
docs: DocumentationUrlsData,
|
||||
urls: DocumentationUrlContainer,
|
||||
): DocumentationUrlContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
} else if (typeof docs === 'string') {
|
||||
urls.addUrl(docs);
|
||||
} else {
|
||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
class DocumentationUrlContainer {
|
||||
private readonly urls = new Array<string>();
|
||||
private readonly urls = new Array<string>();
|
||||
|
||||
public addUrl(url: string) {
|
||||
validateUrl(url);
|
||||
this.urls.push(url);
|
||||
}
|
||||
public addUrl(url: string) {
|
||||
validateUrl(url);
|
||||
this.urls.push(url);
|
||||
}
|
||||
|
||||
public addUrls(urls: readonly any[]) {
|
||||
for (const url of urls) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||
}
|
||||
this.addUrl(url);
|
||||
}
|
||||
public addUrls(urls: readonly string[]) {
|
||||
for (const url of urls) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||
}
|
||||
this.addUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): ReadonlyArray<string> {
|
||||
return this.urls;
|
||||
}
|
||||
public getAll(): ReadonlyArray<string> {
|
||||
return this.urls;
|
||||
}
|
||||
}
|
||||
|
||||
function validateUrl(docUrl: string): void {
|
||||
if (!docUrl) {
|
||||
throw new Error('Documentation url is null or empty');
|
||||
}
|
||||
if (docUrl.includes('\n')) {
|
||||
throw new Error('Documentation url cannot be multi-lined.');
|
||||
}
|
||||
const res = docUrl.match(
|
||||
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
|
||||
if (res == null) {
|
||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||
}
|
||||
if (!docUrl) {
|
||||
throw new Error('Documentation url is null or empty');
|
||||
}
|
||||
if (docUrl.includes('\n')) {
|
||||
throw new Error('Documentation url cannot be multi-lined.');
|
||||
}
|
||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||
const res = docUrl.match(validUrlRegex);
|
||||
if (res == null) {
|
||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
environment: NodeJS.ProcessEnv,
|
||||
): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
@@ -8,15 +8,17 @@ import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||
|
||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||
public readonly compiler: IScriptCompiler;
|
||||
public readonly syntax: ILanguageSyntax;
|
||||
public readonly compiler: IScriptCompiler;
|
||||
|
||||
constructor(
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
||||
if (!scripting) { throw new Error('undefined scripting'); }
|
||||
this.syntax = syntaxFactory.create(scripting.language);
|
||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||
}
|
||||
public readonly syntax: ILanguageSyntax;
|
||||
|
||||
constructor(
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||
) {
|
||||
if (!scripting) { throw new Error('undefined scripting'); }
|
||||
this.syntax = syntaxFactory.create(scripting.language);
|
||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,63 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpression } from './IExpression';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { IExpression } from './IExpression';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||
export class Expression implements IExpression {
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
if (!evaluator) {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters
|
||||
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||
) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('undefined context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(context);
|
||||
if (!evaluator) {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
}
|
||||
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('undefined context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(filteredContext);
|
||||
}
|
||||
}
|
||||
|
||||
function validateThatAllRequiredParametersAreSatisfied(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
const requiredParameterNames = parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
const missingParameterNames = requiredParameterNames
|
||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||
if (missingParameterNames.length) {
|
||||
throw new Error(
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
||||
}
|
||||
const requiredParameterNames = parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
const missingParameterNames = requiredParameterNames
|
||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||
if (missingParameterNames.length) {
|
||||
throw new Error(
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnusedArguments(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameter of parameters.all) {
|
||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
||||
continue; // Optional parameter is not necessarily provided
|
||||
}
|
||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
||||
specificCallArgs.addArgument(arg);
|
||||
}
|
||||
return specificCallArgs;
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||
parameters.all
|
||||
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||
return specificCallArgs;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
||||
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||
|
||||
export interface IExpressionEvaluationContext {
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
readonly pipelineCompiler: IPipelineCompiler;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
readonly pipelineCompiler: IPipelineCompiler;
|
||||
}
|
||||
|
||||
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||
constructor(
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
constructor(
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||
) {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
export class ExpressionPosition {
|
||||
constructor(
|
||||
public readonly start: number,
|
||||
public readonly end: number) {
|
||||
if (start === end) {
|
||||
throw new Error(`no length (start = end = ${start})`);
|
||||
}
|
||||
if (start > end) {
|
||||
throw Error(`start (${start}) after end (${end})`);
|
||||
}
|
||||
if (start < 0) {
|
||||
throw Error(`negative start position: ${start}`);
|
||||
}
|
||||
constructor(
|
||||
public readonly start: number,
|
||||
public readonly end: number,
|
||||
) {
|
||||
if (start === end) {
|
||||
throw new Error(`no length (start = end = ${start})`);
|
||||
}
|
||||
if (start > end) {
|
||||
throw Error(`start (${start}) after end (${end})`);
|
||||
}
|
||||
if (start < 0) {
|
||||
throw Error(`negative start position: ${start}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export interface IExpression {
|
||||
readonly position: ExpressionPosition;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
evaluate(context: IExpressionEvaluationContext): string;
|
||||
readonly position: ExpressionPosition;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
evaluate(context: IExpressionEvaluationContext): string;
|
||||
}
|
||||
|
||||
@@ -1,81 +1,86 @@
|
||||
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||
import { IExpression } from './Expression/IExpression';
|
||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
|
||||
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public constructor(
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compiledCode;
|
||||
public constructor(
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||
) { }
|
||||
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): string {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function compileExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext) {
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = expressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
let index = 0;
|
||||
while (index !== code.length) {
|
||||
const nextExpression = sortedExpressions.pop();
|
||||
if (nextExpression) {
|
||||
compiledCode += code.substring(index, nextExpression.position.start);
|
||||
const expressionCode = nextExpression.evaluate(context);
|
||||
compiledCode += expressionCode;
|
||||
index = nextExpression.position.end;
|
||||
} else {
|
||||
compiledCode += code.substring(index, code.length);
|
||||
break;
|
||||
}
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = expressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
let index = 0;
|
||||
while (index !== code.length) {
|
||||
const nextExpression = sortedExpressions.pop();
|
||||
if (nextExpression) {
|
||||
compiledCode += code.substring(index, nextExpression.position.start);
|
||||
const expressionCode = nextExpression.evaluate(context);
|
||||
compiledCode += expressionCode;
|
||||
index = nextExpression.position.end;
|
||||
} else {
|
||||
compiledCode += code.substring(index, code.length);
|
||||
break;
|
||||
}
|
||||
return compiledCode;
|
||||
}
|
||||
return compiledCode;
|
||||
}
|
||||
|
||||
function extractRequiredParameterNames(
|
||||
expressions: readonly IExpression[]): string[] {
|
||||
const usedParameterNames = expressions
|
||||
.map((e) => e.parameters.all
|
||||
.filter((p) => !p.isOptional)
|
||||
.map((p) => p.name))
|
||||
.filter((p) => p)
|
||||
.flat();
|
||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
||||
return uniqueParameterNames;
|
||||
expressions: readonly IExpression[],
|
||||
): string[] {
|
||||
const usedParameterNames = expressions
|
||||
.map((e) => e.parameters.all
|
||||
.filter((p) => !p.isOptional)
|
||||
.map((p) => p.name))
|
||||
.filter((p) => p)
|
||||
.flat();
|
||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
function ensureParamsUsedInCodeHasArgsProvided(
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = usedParameterNames
|
||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||
if (notProvidedParameters.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||
}
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = usedParameterNames
|
||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||
if (notProvidedParameters.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IExpressionsCompiler {
|
||||
compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||
compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
|
||||
const Parsers = [
|
||||
new ParameterSubstitutionParser(),
|
||||
new WithParser(),
|
||||
new ParameterSubstitutionParser(),
|
||||
new WithParser(),
|
||||
];
|
||||
|
||||
export class CompositeExpressionParser implements IExpressionParser {
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||
if (leafs.some((leaf) => !leaf)) {
|
||||
throw new Error('undefined leaf');
|
||||
}
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||
if (leafs.some((leaf) => !leaf)) {
|
||||
throw new Error('undefined leaf');
|
||||
}
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
const expressions = new Array<IExpression>();
|
||||
for (const parser of this.leafs) {
|
||||
const newExpressions = parser.findExpressions(code);
|
||||
if (newExpressions && newExpressions.length) {
|
||||
expressions.push(...newExpressions);
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
const expressions = new Array<IExpression>();
|
||||
for (const parser of this.leafs) {
|
||||
const newExpressions = parser.findExpressions(code);
|
||||
if (newExpressions && newExpressions.length) {
|
||||
expressions.push(...newExpressions);
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
|
||||
export interface IExpressionParser {
|
||||
findExpressions(code: string): IExpression[];
|
||||
findExpressions(code: string): IExpression[];
|
||||
}
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
export class ExpressionRegexBuilder {
|
||||
private readonly parts = new Array<string>();
|
||||
private readonly parts = new Array<string>();
|
||||
|
||||
public expectCharacters(characters: string) {
|
||||
return this.addRawRegex(
|
||||
characters
|
||||
.replaceAll('$', '\\$')
|
||||
.replaceAll('.', '\\.'),
|
||||
);
|
||||
}
|
||||
public expectCharacters(characters: string) {
|
||||
return this.addRawRegex(
|
||||
characters
|
||||
.replaceAll('$', '\\$')
|
||||
.replaceAll('.', '\\.'),
|
||||
);
|
||||
}
|
||||
|
||||
public expectOneOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s+');
|
||||
}
|
||||
public expectOneOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s+');
|
||||
}
|
||||
|
||||
public matchPipeline() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(\\|\\s*.+?)?');
|
||||
}
|
||||
public matchPipeline() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(\\|\\s*.+?)?');
|
||||
}
|
||||
|
||||
public matchUntilFirstWhitespace() {
|
||||
return this
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
public matchUntilFirstWhitespace() {
|
||||
return this
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
public expectExpressionStart() {
|
||||
return this
|
||||
.expectCharacters('{{')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
public expectExpressionStart() {
|
||||
return this
|
||||
.expectCharacters('{{')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
public expectExpressionEnd() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.expectCharacters('}}');
|
||||
}
|
||||
public expectExpressionEnd() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.expectCharacters('}}');
|
||||
}
|
||||
|
||||
public buildRegExp(): RegExp {
|
||||
return new RegExp(this.parts.join(''), 'g');
|
||||
}
|
||||
public buildRegExp(): RegExp {
|
||||
return new RegExp(this.parts.join(''), 'g');
|
||||
}
|
||||
|
||||
private expectZeroOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s*');
|
||||
}
|
||||
private addRawRegex(regex: string) {
|
||||
this.parts.push(regex);
|
||||
return this;
|
||||
}
|
||||
private expectZeroOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s*');
|
||||
}
|
||||
|
||||
private addRawRegex(regex: string) {
|
||||
this.parts.push(regex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,46 +6,47 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
|
||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
protected abstract readonly regex: RegExp;
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error('undefined code');
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error('undefined code');
|
||||
}
|
||||
const matches = Array.from(code.matchAll(this.regex));
|
||||
for (const match of matches) {
|
||||
const startPos = match.index;
|
||||
const endPos = startPos + match[0].length;
|
||||
let position: ExpressionPosition;
|
||||
try {
|
||||
position = new ExpressionPosition(startPos, endPos);
|
||||
} catch (error) {
|
||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
||||
}
|
||||
const primitiveExpression = this.buildExpression(match);
|
||||
const parameters = getParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
yield expression;
|
||||
}
|
||||
const matches = Array.from(code.matchAll(this.regex));
|
||||
for (const match of matches) {
|
||||
const startPos = match.index;
|
||||
const endPos = startPos + match[0].length;
|
||||
let position: ExpressionPosition;
|
||||
try {
|
||||
position = new ExpressionPosition(startPos, endPos);
|
||||
} catch (error) {
|
||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
||||
}
|
||||
const primitiveExpression = this.buildExpression(match);
|
||||
const parameters = getParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
yield expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPrimitiveExpression {
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly IFunctionParameter[];
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly IFunctionParameter[];
|
||||
}
|
||||
|
||||
function getParameters(
|
||||
expression: IPrimitiveExpression): FunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
for (const parameter of expression.parameters || []) {
|
||||
parameters.addParameter(parameter);
|
||||
}
|
||||
return parameters;
|
||||
expression: IPrimitiveExpression,
|
||||
): FunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
for (const parameter of expression.parameters || []) {
|
||||
parameters.addParameter(parameter);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IPipe {
|
||||
readonly name: string;
|
||||
apply(input: string): string;
|
||||
readonly name: string;
|
||||
apply(input: string): string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface IPipelineCompiler {
|
||||
compile(value: string, pipeline: string): string;
|
||||
compile(value: string, pipeline: string): string;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { IPipe } from '../IPipe';
|
||||
|
||||
export class EscapeDoubleQuotes implements IPipe {
|
||||
public readonly name: string = 'escapeDoubleQuotes';
|
||||
public apply(raw: string): string {
|
||||
return raw?.replaceAll('"', '"^""');
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
""
|
||||
Breaks, because it is fundamentally unsupported
|
||||
""""
|
||||
Does not work with consecutive double quotes.
|
||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
||||
\"
|
||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
|
||||
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
|
||||
\""
|
||||
Normalizes interior whitespace
|
||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
}
|
||||
public readonly name: string = 'escapeDoubleQuotes';
|
||||
|
||||
public apply(raw: string): string {
|
||||
return raw?.replaceAll('"', '"^""');
|
||||
/* eslint-disable max-len */
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
""
|
||||
Breaks, because it is fundamentally unsupported
|
||||
""""
|
||||
Does not work with consecutive double quotes.
|
||||
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||
\"
|
||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
|
||||
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||
\""
|
||||
Normalizes interior whitespace
|
||||
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,166 @@
|
||||
import { IPipe } from '../IPipe';
|
||||
|
||||
export class InlinePowerShell implements IPipe {
|
||||
public readonly name: string = 'inlinePowerShell';
|
||||
public apply(code: string): string {
|
||||
if (!code || !hasLines(code)) {
|
||||
return code;
|
||||
}
|
||||
code = inlineComments(code);
|
||||
code = mergeLinesWithBacktick(code);
|
||||
code = mergeHereStrings(code);
|
||||
const lines = getLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
return lines
|
||||
.join('; ');
|
||||
public readonly name: string = 'inlinePowerShell';
|
||||
|
||||
public apply(code: string): string {
|
||||
if (!code || !hasLines(code)) {
|
||||
return code;
|
||||
}
|
||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||
inlineComments,
|
||||
mergeLinesWithBacktick,
|
||||
mergeHereStrings,
|
||||
mergeNewLines,
|
||||
]).reduce((a, b) => (data) => b(a(data)));
|
||||
const newCode = processor(code);
|
||||
return newCode;
|
||||
}
|
||||
}
|
||||
|
||||
function hasLines(text: string) {
|
||||
return text.includes('\n') || text.includes('\r');
|
||||
return text.includes('\n') || text.includes('\r');
|
||||
}
|
||||
|
||||
/*
|
||||
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||
Otherwise single # comments out rest of the code
|
||||
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||
Otherwise single # comments out rest of the code
|
||||
*/
|
||||
function inlineComments(code: string): string {
|
||||
const makeInlineComment = (comment: string) => {
|
||||
const value = comment?.trim();
|
||||
if (!value) {
|
||||
return '<##>';
|
||||
}
|
||||
return `<# ${value} #>`;
|
||||
};
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
const makeInlineComment = (comment: string) => {
|
||||
const value = comment?.trim();
|
||||
if (!value) {
|
||||
return '<##>';
|
||||
}
|
||||
return `<# ${value} #>`;
|
||||
};
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
/*
|
||||
Other alternatives considered:
|
||||
--------------------------
|
||||
/#(?<!<#)(?![<>])(.*)$/gm
|
||||
-------------------------
|
||||
✅ Simple, yet matches and captures only what's necessary
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||
❌ Uses lookbehind
|
||||
Safari does not yet support lookbehind and syntax, leading application to not
|
||||
load and throw "Invalid regular expression: invalid group specifier name"
|
||||
https://caniuse.com/js-regexp-lookbehind
|
||||
⏩ Usage
|
||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||
return makeInlineComment(captureComment)
|
||||
});
|
||||
----------------
|
||||
/<#.*?#>|#(.*)/g
|
||||
----------------
|
||||
✅ Simple yet affective
|
||||
❌ Matches all comments, but only captures dash comments
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
⏩ Usage
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
/*
|
||||
Other alternatives considered:
|
||||
--------------------------
|
||||
/#(?<!<#)(?![<>])(.*)$/gm
|
||||
-------------------------
|
||||
✅ Simple, yet matches and captures only what's necessary
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||
❌ Uses lookbehind
|
||||
Safari does not yet support lookbehind and syntax, leading application to not
|
||||
load and throw "Invalid regular expression: invalid group specifier name"
|
||||
https://caniuse.com/js-regexp-lookbehind
|
||||
⏩ Usage
|
||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||
return makeInlineComment(captureComment)
|
||||
});
|
||||
----------------
|
||||
/<#.*?#>|#(.*)/g
|
||||
----------------
|
||||
✅ Simple yet affective
|
||||
❌ Matches all comments, but only captures dash comments
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
⏩ Usage
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
------------------------------------
|
||||
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||
------------------------------------
|
||||
✅ Covers all cases
|
||||
❌ Matches every line, three capture groups are used to build result
|
||||
⏩ Usage
|
||||
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||
(match, captureLeft, captureDash, captureComment) => {
|
||||
if (!captureDash) {
|
||||
return match;
|
||||
}
|
||||
return captureLeft + makeInlineComment(captureComment);
|
||||
});
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
------------------------------------
|
||||
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||
------------------------------------
|
||||
✅ Covers all cases
|
||||
❌ Matches every line, three capture groups are used to build result
|
||||
⏩ Usage
|
||||
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||
(match, captureLeft, captureDash, captureComment) => {
|
||||
if (!captureDash) {
|
||||
return match;
|
||||
}
|
||||
return captureLeft + makeInlineComment(captureComment);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
function getLines(code: string): string [] {
|
||||
return (code?.split(/\r\n|\r|\n/) || []);
|
||||
function getLines(code: string): string[] {
|
||||
return (code?.split(/\r\n|\r|\n/) || []);
|
||||
}
|
||||
|
||||
/*
|
||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||
*/
|
||||
function mergeHereStrings(code: string) {
|
||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||
const newString = getHereStringHandler(quotes);
|
||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||
const lines = getLines(escaped);
|
||||
const inlined = lines.join(newString.separator);
|
||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||
return quoted;
|
||||
});
|
||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||
const newString = getHereStringHandler(quotes);
|
||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||
const lines = getLines(escaped);
|
||||
const inlined = lines.join(newString.separator);
|
||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||
return quoted;
|
||||
});
|
||||
}
|
||||
interface IInlinedHereString {
|
||||
readonly quotesAround: string;
|
||||
readonly escapedQuotes: string;
|
||||
readonly separator: string;
|
||||
readonly quotesAround: string;
|
||||
readonly escapedQuotes: string;
|
||||
readonly separator: string;
|
||||
}
|
||||
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||
const expandableNewLine = '`r`n';
|
||||
switch (quotes) {
|
||||
case '\'':
|
||||
return {
|
||||
quotesAround: '\'',
|
||||
escapedQuotes: '\'\'',
|
||||
separator: `\'+"${expandableNewLine}"+\'`,
|
||||
};
|
||||
case '"':
|
||||
return {
|
||||
quotesAround: '"',
|
||||
escapedQuotes: '`"',
|
||||
separator: expandableNewLine,
|
||||
};
|
||||
default:
|
||||
throw new Error(`expected quotes: ${quotes}`);
|
||||
}
|
||||
/*
|
||||
We handle @' and @" differently.
|
||||
Single quotes are interpreted literally and doubles are expandable.
|
||||
*/
|
||||
const expandableNewLine = '`r`n';
|
||||
switch (quotes) {
|
||||
case '\'':
|
||||
return {
|
||||
quotesAround: '\'',
|
||||
escapedQuotes: '\'\'',
|
||||
separator: `'+"${expandableNewLine}"+'`,
|
||||
};
|
||||
case '"':
|
||||
return {
|
||||
quotesAround: '"',
|
||||
escapedQuotes: '`"',
|
||||
separator: expandableNewLine,
|
||||
};
|
||||
default:
|
||||
throw new Error(`expected quotes: ${quotes}`);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Input ->
|
||||
Get-Service * `
|
||||
Sort-Object StartType `
|
||||
Format-Table Name, ServiceType, Status -AutoSize
|
||||
Output ->
|
||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||
Input ->
|
||||
Get-Service * `
|
||||
Sort-Object StartType `
|
||||
Format-Table Name, ServiceType, Status -AutoSize
|
||||
Output ->
|
||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||
*/
|
||||
function mergeLinesWithBacktick(code: string) {
|
||||
/*
|
||||
The regex actually wraps any whitespace character after backtick and before newline
|
||||
However, this is not always the case for PowerShell.
|
||||
I see two behaviors:
|
||||
1. If inside string, it's accepted (inside " or ')
|
||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||
we wrap it anyway
|
||||
*/
|
||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||
/*
|
||||
The regex actually wraps any whitespace character after backtick and before newline
|
||||
However, this is not always the case for PowerShell.
|
||||
I see two behaviors:
|
||||
1. If inside string, it's accepted (inside " or ')
|
||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||
we wrap it anyway
|
||||
*/
|
||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||
}
|
||||
|
||||
function mergeNewLines(code: string) {
|
||||
return getLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
@@ -3,45 +3,48 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||
|
||||
const RegisteredPipes = [
|
||||
new EscapeDoubleQuotes(),
|
||||
new InlinePowerShell(),
|
||||
new EscapeDoubleQuotes(),
|
||||
new InlinePowerShell(),
|
||||
];
|
||||
|
||||
export interface IPipeFactory {
|
||||
get(pipeName: string): IPipe;
|
||||
get(pipeName: string): IPipe;
|
||||
}
|
||||
|
||||
export class PipeFactory implements IPipeFactory {
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('undefined pipe in list');
|
||||
}
|
||||
for (const pipe of pipes) {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('undefined pipe in list');
|
||||
}
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
for (const pipe of pipes) {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
validatePipeName(pipe.name);
|
||||
if (this.pipes.has(pipe.name)) {
|
||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||
}
|
||||
this.pipes.set(pipe.name, pipe);
|
||||
}
|
||||
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
}
|
||||
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
validatePipeName(pipe.name);
|
||||
if (this.pipes.has(pipe.name)) {
|
||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||
}
|
||||
this.pipes.set(pipe.name, pipe);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePipeName(name: string) {
|
||||
if (!name) {
|
||||
throw new Error('empty pipe name');
|
||||
}
|
||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||
}
|
||||
if (!name) {
|
||||
throw new Error('empty pipe name');
|
||||
}
|
||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,32 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
|
||||
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||
|
||||
export class PipelineCompiler implements IPipelineCompiler {
|
||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||
public compile(value: string, pipeline: string): string {
|
||||
ensureValidArguments(value, pipeline);
|
||||
const pipeNames = extractPipeNames(pipeline);
|
||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||
for (const pipe of pipes) {
|
||||
value = pipe.apply(value);
|
||||
}
|
||||
return value;
|
||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||
|
||||
public compile(value: string, pipeline: string): string {
|
||||
ensureValidArguments(value, pipeline);
|
||||
const pipeNames = extractPipeNames(pipeline);
|
||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||
let valueInCompilation = value;
|
||||
for (const pipe of pipes) {
|
||||
valueInCompilation = pipe.apply(valueInCompilation);
|
||||
}
|
||||
return valueInCompilation;
|
||||
}
|
||||
}
|
||||
|
||||
function extractPipeNames(pipeline: string): string[] {
|
||||
return pipeline
|
||||
.trim()
|
||||
.split('|')
|
||||
.slice(1)
|
||||
.map((p) => p.trim());
|
||||
return pipeline
|
||||
.trim()
|
||||
.split('|')
|
||||
.slice(1)
|
||||
.map((p) => p.trim());
|
||||
}
|
||||
|
||||
function ensureValidArguments(value: string, pipeline: string) {
|
||||
if (!value) { throw new Error('undefined value'); }
|
||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||
if (!pipeline.trimStart().startsWith('|')) {
|
||||
throw new Error('pipeline does not start with pipe');
|
||||
}
|
||||
if (!value) { throw new Error('undefined value'); }
|
||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||
if (!pipeline.trimStart().startsWith('|')) {
|
||||
throw new Error('pipeline does not start with pipe');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class ParameterSubstitutionParser extends RegexParser {
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||
.matchPipeline() // Second match: Pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||
.matchPipeline() // Second match: Pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const pipeline = match[2];
|
||||
return {
|
||||
parameters: [ new FunctionParameter(parameterName, false) ],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
},
|
||||
};
|
||||
}
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const pipeline = match[2];
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, false)],
|
||||
evaluator: (context) => {
|
||||
const { argumentValue } = context.args.getArgument(parameterName);
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class WithParser extends RegexParser {
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
// {{ with $parameterName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('with')
|
||||
.expectOneOrMoreWhitespaces()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const scopeText = match[2];
|
||||
return {
|
||||
parameters: [ new FunctionParameter(parameterName, true) ],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
||||
context.args.getArgument(parameterName).argumentValue
|
||||
: undefined;
|
||||
if (!argumentValue) {
|
||||
return '';
|
||||
}
|
||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||
// {{ . | pipeName }}
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
// {{ with $parameterName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('.')
|
||||
.matchPipeline() // First match: pipeline
|
||||
.expectCharacters('with')
|
||||
.expectOneOrMoreWhitespaces()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
||||
return replacer(match1);
|
||||
});
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const scopeText = match[2];
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, true)],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.hasArgument(parameterName)
|
||||
? context.args.getArgument(parameterName).argumentValue
|
||||
: undefined;
|
||||
if (!argumentValue) {
|
||||
return '';
|
||||
}
|
||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||
// {{ . | pipeName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('.')
|
||||
.matchPipeline() // First match: pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||
// but instead letting the pipeline compiler to fail on those.
|
||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||
return replacer(match1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
|
||||
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||
constructor(
|
||||
public readonly parameterName: string,
|
||||
public readonly argumentValue: string) {
|
||||
ensureValidParameterName(parameterName);
|
||||
if (!argumentValue) {
|
||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||
}
|
||||
constructor(
|
||||
public readonly parameterName: string,
|
||||
public readonly argumentValue: string,
|
||||
) {
|
||||
ensureValidParameterName(parameterName);
|
||||
if (!argumentValue) {
|
||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,33 +2,37 @@ import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||
|
||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||
public addArgument(argument: IFunctionCallArgument): void {
|
||||
if (!argument) {
|
||||
throw new Error('undefined argument');
|
||||
}
|
||||
if (this.hasArgument(argument.parameterName)) {
|
||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||
}
|
||||
this.arguments.set(argument.parameterName, argument);
|
||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||
|
||||
public addArgument(argument: IFunctionCallArgument): void {
|
||||
if (!argument) {
|
||||
throw new Error('undefined argument');
|
||||
}
|
||||
public getAllParameterNames(): string[] {
|
||||
return Array.from(this.arguments.keys());
|
||||
if (this.hasArgument(argument.parameterName)) {
|
||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||
}
|
||||
public hasArgument(parameterName: string): boolean {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
return this.arguments.has(parameterName);
|
||||
this.arguments.set(argument.parameterName, argument);
|
||||
}
|
||||
|
||||
public getAllParameterNames(): string[] {
|
||||
return Array.from(this.arguments.keys());
|
||||
}
|
||||
|
||||
public hasArgument(parameterName: string): boolean {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
const arg = this.arguments.get(parameterName);
|
||||
if (!arg) {
|
||||
throw new Error(`parameter does not exist: ${parameterName}`);
|
||||
}
|
||||
return arg;
|
||||
return this.arguments.has(parameterName);
|
||||
}
|
||||
|
||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
const arg = this.arguments.get(parameterName);
|
||||
if (!arg) {
|
||||
throw new Error(`parameter does not exist: ${parameterName}`);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IFunctionCallArgument {
|
||||
readonly parameterName: string;
|
||||
readonly argumentValue: string;
|
||||
readonly parameterName: string;
|
||||
readonly argumentValue: string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
|
||||
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||
getArgument(parameterName: string): IFunctionCallArgument;
|
||||
getAllParameterNames(): string[];
|
||||
hasArgument(parameterName: string): boolean;
|
||||
getArgument(parameterName: string): IFunctionCallArgument;
|
||||
getAllParameterNames(): string[];
|
||||
hasArgument(parameterName: string): boolean;
|
||||
}
|
||||
|
||||
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||
addArgument(argument: IFunctionCallArgument): void;
|
||||
addArgument(argument: IFunctionCallArgument): void;
|
||||
}
|
||||
|
||||
@@ -1,139 +1,149 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
|
||||
}
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
if (!functions) { throw new Error('undefined functions'); }
|
||||
if (!calls) { throw new Error('undefined calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||
const context: ICompilationContext = {
|
||||
allFunctions: functions,
|
||||
callSequence: calls,
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
};
|
||||
const code = compileCallSequence(context);
|
||||
return code;
|
||||
}
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection,
|
||||
): ICompiledCode {
|
||||
if (!functions) { throw new Error('undefined functions'); }
|
||||
if (!calls) { throw new Error('undefined calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||
const context: ICompilationContext = {
|
||||
allFunctions: functions,
|
||||
callSequence: calls,
|
||||
expressionsCompiler: this.expressionsCompiler,
|
||||
};
|
||||
const code = compileCallSequence(context);
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompilationContext {
|
||||
allFunctions: ISharedFunctionCollection;
|
||||
callSequence: readonly IFunctionCall[];
|
||||
expressionsCompiler: IExpressionsCompiler;
|
||||
allFunctions: ISharedFunctionCollection;
|
||||
callSequence: readonly IFunctionCall[];
|
||||
expressionsCompiler: IExpressionsCompiler;
|
||||
}
|
||||
|
||||
interface ICompiledFunctionCall {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
|
||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||
const compiledFunctions = new Array<ICompiledFunctionCall>();
|
||||
for (const call of context.callSequence) {
|
||||
const compiledCode = compileSingleCall(call, context);
|
||||
compiledFunctions.push(...compiledCode);
|
||||
}
|
||||
return {
|
||||
code: merge(compiledFunctions.map((f) => f.code)),
|
||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||
};
|
||||
const compiledFunctions = new Array<ICompiledFunctionCall>();
|
||||
for (const call of context.callSequence) {
|
||||
const compiledCode = compileSingleCall(call, context);
|
||||
compiledFunctions.push(...compiledCode);
|
||||
}
|
||||
return {
|
||||
code: merge(compiledFunctions.map((f) => f.code)),
|
||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||
};
|
||||
}
|
||||
|
||||
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [ compiledCode ];
|
||||
} else { // Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||
return compileSingleCall(compiledCall, context);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
function compileSingleCall(
|
||||
call: IFunctionCall,
|
||||
context: ICompilationContext,
|
||||
): ICompiledFunctionCall[] {
|
||||
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [compiledCode];
|
||||
}
|
||||
// Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||
return compileSingleCall(compiledCall, context);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.do, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.do, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
};
|
||||
}
|
||||
|
||||
function compileArgs(
|
||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const compiledArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
||||
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
||||
compiledArgs.addArgument(newArgument);
|
||||
}
|
||||
return compiledArgs;
|
||||
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const compiledArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
||||
compiledArgs.addArgument(newArgument);
|
||||
}
|
||||
return compiledArgs;
|
||||
}
|
||||
|
||||
function merge(codeParts: readonly string[]): string {
|
||||
return codeParts
|
||||
.filter((part) => part?.length > 0)
|
||||
.join('\n');
|
||||
return codeParts
|
||||
.filter((part) => part?.length > 0)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||
}
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[]): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
return callArgumentNames
|
||||
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||
}
|
||||
|
||||
function throwIfNotEmpty(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[]) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: ` +
|
||||
`"${unexpectedParameters.join('", "')}"` +
|
||||
'. Expected parameter(s): ' +
|
||||
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { IFunctionCall } from '../IFunctionCall';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCal
|
||||
import { IFunctionCall } from './IFunctionCall';
|
||||
|
||||
export class FunctionCall implements IFunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
|
||||
if (!functionName) {
|
||||
throw new Error('empty function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('undefined args');
|
||||
}
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('empty function name in function call');
|
||||
}
|
||||
if (!args) {
|
||||
throw new Error('undefined args');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,31 +5,31 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||
if (!calls) {
|
||||
throw new Error('undefined call data');
|
||||
}
|
||||
const sequence = getCallSequence(calls);
|
||||
return sequence.map((call) => parseFunctionCall(call));
|
||||
if (calls === undefined) {
|
||||
throw new Error('undefined call data');
|
||||
}
|
||||
const sequence = getCallSequence(calls);
|
||||
return sequence.map((call) => parseFunctionCall(call));
|
||||
}
|
||||
|
||||
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
if (typeof calls !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
}
|
||||
if (calls instanceof Array) {
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
return [ calls as FunctionCallData ];
|
||||
if (typeof calls !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
}
|
||||
if (calls instanceof Array) {
|
||||
return calls as FunctionCallData[];
|
||||
}
|
||||
return [calls as FunctionCallData];
|
||||
}
|
||||
|
||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call`);
|
||||
}
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of Object.keys(call.parameters || {})) {
|
||||
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
|
||||
args.addArgument(arg);
|
||||
}
|
||||
return new FunctionCall(call.function, args);
|
||||
if (!call) {
|
||||
throw new Error('undefined function call');
|
||||
}
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of Object.keys(call.parameters || {})) {
|
||||
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
|
||||
args.addArgument(arg);
|
||||
}
|
||||
return new FunctionCall(call.function, args);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IFunctionCall {
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
readonly functionName: string;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
readonly body: ISharedFunctionBody;
|
||||
readonly name: string;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
readonly body: ISharedFunctionBody;
|
||||
}
|
||||
|
||||
export interface ISharedFunctionBody {
|
||||
readonly type: FunctionBodyType;
|
||||
readonly code: IFunctionCode;
|
||||
readonly calls: readonly IFunctionCall[];
|
||||
readonly type: FunctionBodyType;
|
||||
readonly code: IFunctionCode;
|
||||
readonly calls: readonly IFunctionCall[];
|
||||
}
|
||||
|
||||
export enum FunctionBodyType {
|
||||
Code,
|
||||
Calls,
|
||||
Code,
|
||||
Calls,
|
||||
}
|
||||
|
||||
export interface IFunctionCode {
|
||||
readonly do: string;
|
||||
readonly revert?: string;
|
||||
readonly do: string;
|
||||
readonly revert?: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export interface ISharedFunctionsParser {
|
||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
|
||||
export class FunctionParameter implements IFunctionParameter {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly isOptional: boolean) {
|
||||
ensureValidParameterName(name);
|
||||
}
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly isOptional: boolean,
|
||||
) {
|
||||
ensureValidParameterName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,27 @@ import { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
|
||||
export class FunctionParameterCollection implements IFunctionParameterCollection {
|
||||
private parameters = new Array<IFunctionParameter>();
|
||||
private parameters = new Array<IFunctionParameter>();
|
||||
|
||||
public get all(): readonly IFunctionParameter[] {
|
||||
return this.parameters;
|
||||
}
|
||||
public addParameter(parameter: IFunctionParameter) {
|
||||
this.ensureValidParameter(parameter);
|
||||
this.parameters.push(parameter);
|
||||
}
|
||||
public get all(): readonly IFunctionParameter[] {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
private includesName(name: string) {
|
||||
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
||||
public addParameter(parameter: IFunctionParameter) {
|
||||
this.ensureValidParameter(parameter);
|
||||
this.parameters.push(parameter);
|
||||
}
|
||||
|
||||
private includesName(name: string) {
|
||||
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
||||
}
|
||||
|
||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||
if (!parameter) {
|
||||
throw new Error('undefined parameter');
|
||||
}
|
||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||
if (!parameter) {
|
||||
throw new Error('undefined parameter');
|
||||
}
|
||||
if (this.includesName(parameter.name)) {
|
||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||
}
|
||||
if (this.includesName(parameter.name)) {
|
||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IFunctionParameter {
|
||||
readonly name: string;
|
||||
readonly isOptional: boolean;
|
||||
readonly name: string;
|
||||
readonly isOptional: boolean;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
|
||||
export interface IReadOnlyFunctionParameterCollection {
|
||||
readonly all: readonly IFunctionParameter[];
|
||||
readonly all: readonly IFunctionParameter[];
|
||||
}
|
||||
|
||||
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
|
||||
addParameter(parameter: IFunctionParameter): void;
|
||||
addParameter(parameter: IFunctionParameter): void;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export function ensureValidParameterName(parameterName: string) {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
|
||||
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
|
||||
}
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
|
||||
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,54 @@
|
||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
} from './ISharedFunction';
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
|
||||
export function createCallerFunction(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly IFunctionCall[]): ISharedFunction {
|
||||
if (!callSequence) {
|
||||
throw new Error(`undefined call sequence in function "${name}"`);
|
||||
}
|
||||
if (!callSequence.length) {
|
||||
throw new Error(`empty call sequence in function "${name}"`);
|
||||
}
|
||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly IFunctionCall[],
|
||||
): ISharedFunction {
|
||||
if (!callSequence) {
|
||||
throw new Error(`undefined call sequence in function "${name}"`);
|
||||
}
|
||||
if (!callSequence.length) {
|
||||
throw new Error(`empty call sequence in function "${name}"`);
|
||||
}
|
||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||
}
|
||||
|
||||
export function createFunctionWithInlineCode(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
code: string,
|
||||
revertCode?: string): ISharedFunction {
|
||||
if (!code) {
|
||||
throw new Error(`undefined code in function "${name}"`);
|
||||
}
|
||||
const content: IFunctionCode = {
|
||||
do: code,
|
||||
revert: revertCode,
|
||||
};
|
||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
code: string,
|
||||
revertCode?: string,
|
||||
): ISharedFunction {
|
||||
if (!code) {
|
||||
throw new Error(`undefined code in function "${name}"`);
|
||||
}
|
||||
const content: IFunctionCode = {
|
||||
do: code,
|
||||
revert: revertCode,
|
||||
};
|
||||
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||
}
|
||||
|
||||
class SharedFunction implements ISharedFunction {
|
||||
public readonly body: ISharedFunctionBody;
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||
content: IFunctionCode | readonly IFunctionCall[],
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('undefined function name'); }
|
||||
if (!parameters) { throw new Error(`undefined parameters`); }
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
|
||||
};
|
||||
}
|
||||
public readonly body: ISharedFunctionBody;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||
content: IFunctionCode | readonly IFunctionCall[],
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('undefined function name'); }
|
||||
if (!parameters) { throw new Error('undefined parameters'); }
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||
|
||||
public addFunction(func: ISharedFunction): void {
|
||||
if (!func) { throw new Error('undefined function'); }
|
||||
if (this.has(func.name)) {
|
||||
throw new Error(`function with name ${func.name} already exists`);
|
||||
}
|
||||
this.functionsByName.set(func.name, func);
|
||||
public addFunction(func: ISharedFunction): void {
|
||||
if (!func) { throw new Error('undefined function'); }
|
||||
if (this.has(func.name)) {
|
||||
throw new Error(`function with name ${func.name} already exists`);
|
||||
}
|
||||
this.functionsByName.set(func.name, func);
|
||||
}
|
||||
|
||||
public getFunctionByName(name: string): ISharedFunction {
|
||||
if (!name) { throw Error('undefined function name'); }
|
||||
const func = this.functionsByName.get(name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
public getFunctionByName(name: string): ISharedFunction {
|
||||
if (!name) { throw Error('undefined function name'); }
|
||||
const func = this.functionsByName.get(name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
private has(functionName: string) {
|
||||
return this.functionsByName.has(functionName);
|
||||
}
|
||||
private has(functionName: string) {
|
||||
return this.functionsByName.has(functionName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,131 +10,132 @@ import { ISharedFunction } from './ISharedFunction';
|
||||
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||
|
||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||
public parseFunctions(
|
||||
functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
for (const func of functions) {
|
||||
const sharedFunction = parseFunction(func);
|
||||
collection.addFunction(sharedFunction);
|
||||
}
|
||||
return collection;
|
||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||
|
||||
public parseFunctions(
|
||||
functions: readonly FunctionData[],
|
||||
): ISharedFunctionCollection {
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
for (const func of functions) {
|
||||
const sharedFunction = parseFunction(func);
|
||||
collection.addFunction(sharedFunction);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFunction(data: FunctionData): ISharedFunction {
|
||||
const name = data.name;
|
||||
const parameters = parseParameters(data);
|
||||
if (hasCode(data)) {
|
||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||
} else { // has call
|
||||
const calls = parseFunctionCalls(data.call);
|
||||
return createCallerFunction(name, parameters, calls);
|
||||
}
|
||||
const { name } = data;
|
||||
const parameters = parseParameters(data);
|
||||
if (hasCode(data)) {
|
||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||
}
|
||||
// Has call
|
||||
const calls = parseFunctionCalls(data.call);
|
||||
return createCallerFunction(name, parameters, calls);
|
||||
}
|
||||
|
||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
if (!data.parameters) {
|
||||
return parameters;
|
||||
}
|
||||
for (const parameterData of data.parameters) {
|
||||
const isOptional = parameterData.optional || false;
|
||||
try {
|
||||
const parameter = new FunctionParameter(parameterData.name, isOptional);
|
||||
parameters.addParameter(parameter);
|
||||
} catch (err) {
|
||||
throw new Error(`"${data.name}": ${err.message}`);
|
||||
}
|
||||
}
|
||||
const parameters = new FunctionParameterCollection();
|
||||
if (!data.parameters) {
|
||||
return parameters;
|
||||
}
|
||||
for (const parameterData of data.parameters) {
|
||||
const isOptional = parameterData.optional || false;
|
||||
try {
|
||||
const parameter = new FunctionParameter(parameterData.name, isOptional);
|
||||
parameters.addParameter(parameter);
|
||||
} catch (err) {
|
||||
throw new Error(`"${data.name}": ${err.message}`);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
function hasCode(data: FunctionData): boolean {
|
||||
return Boolean(data.code);
|
||||
return Boolean(data.code);
|
||||
}
|
||||
|
||||
function hasCall(data: FunctionData): boolean {
|
||||
return Boolean(data.call);
|
||||
return Boolean(data.call);
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
ensureExpectedParametersType(functions);
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
ensureExpectedParametersType(functions);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
if (withBothCallAndCode.length) {
|
||||
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
||||
}
|
||||
// Ensure functions have either code or call
|
||||
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
||||
if (hasEitherCodeOrCall.length) {
|
||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||
}
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
if (withBothCallAndCode.length) {
|
||||
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
||||
}
|
||||
// Ensure functions have either code or call
|
||||
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
||||
if (hasEitherCodeOrCall.length) {
|
||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExpectedParametersType(functions: readonly FunctionData[]) {
|
||||
const unexpectedFunctions = functions
|
||||
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
|
||||
if (unexpectedFunctions.length) {
|
||||
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const unexpectedFunctions = functions
|
||||
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
|
||||
if (unexpectedFunctions.length) {
|
||||
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function isArrayOfObjects(value: any): boolean {
|
||||
return Array.isArray(value)
|
||||
&& value.every((item) => typeof item === 'object');
|
||||
function isArrayOfObjects(value: unknown): boolean {
|
||||
return Array.isArray(value)
|
||||
&& value.every((item) => typeof item === 'object');
|
||||
}
|
||||
|
||||
function printNames(holders: readonly InstructionHolder[]) {
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error('some functions are undefined');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code),
|
||||
);
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
|
||||
export interface IScriptCompiler {
|
||||
canCompile(script: ScriptData): boolean;
|
||||
compile(script: ScriptData): IScriptCode;
|
||||
canCompile(script: ScriptData): boolean;
|
||||
compile(script: ScriptData): IScriptCode;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||
@@ -11,34 +10,38 @@ import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
constructor(
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
|
||||
constructor(
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||
}
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
try {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
try {
|
||||
const calls = parseFunctionCalls(script.call);
|
||||
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
|
||||
export interface ICategoryCollectionParseContext {
|
||||
readonly compiler: IScriptCompiler;
|
||||
readonly syntax: ILanguageSyntax;
|
||||
readonly compiler: IScriptCompiler;
|
||||
readonly syntax: ILanguageSyntax;
|
||||
}
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { parseDocUrls } from '../DocumentationParser';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { parseDocUrls } from '../DocumentationParser';
|
||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
|
||||
export function parseScript(
|
||||
data: ScriptData, context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||
validateScript(data);
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
const script = new Script(
|
||||
/* name */ data.name,
|
||||
/* code */ parseCode(data, context),
|
||||
/* docs */ parseDocUrls(data),
|
||||
/* level */ parseLevel(data.recommend, levelParser));
|
||||
return script;
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel),
|
||||
): Script {
|
||||
validateScript(data);
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
const script = new Script(
|
||||
/* name: */ data.name,
|
||||
/* code: */ parseCode(data, context),
|
||||
/* docs: */ parseDocUrls(data),
|
||||
/* level: */ parseLevel(data.recommend, levelParser),
|
||||
);
|
||||
return script;
|
||||
}
|
||||
|
||||
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
return parser.parseEnum(level, 'level');
|
||||
function parseLevel(
|
||||
level: string,
|
||||
parser: IEnumParser<RecommendationLevel>,
|
||||
): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
return parser.parseEnum(level, 'level');
|
||||
}
|
||||
|
||||
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
||||
}
|
||||
|
||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||
if (script.code && script.call) {
|
||||
throw new Error('cannot define both "call" and "code"');
|
||||
}
|
||||
if (script.revertCode && script.call) {
|
||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||
}
|
||||
if (script.code && script.call) {
|
||||
throw new Error('cannot define both "call" and "code"');
|
||||
}
|
||||
if (script.revertCode && script.call) {
|
||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||
}
|
||||
}
|
||||
|
||||
function validateScript(script: ScriptData) {
|
||||
if (!script) {
|
||||
throw new Error('undefined script');
|
||||
}
|
||||
if (!script.code && !script.call) {
|
||||
throw new Error('must define either "call" or "code"');
|
||||
}
|
||||
ensureNotBothCallAndCode(script);
|
||||
if (!script) {
|
||||
throw new Error('undefined script');
|
||||
}
|
||||
if (!script.code && !script.call) {
|
||||
throw new Error('must define either "call" or "code"');
|
||||
}
|
||||
ensureNotBothCallAndCode(script);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
|
||||
const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ];
|
||||
const PowerShellCommonCodeParts = [ '{', '}' ];
|
||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||
const PowerShellCommonCodeParts = ['{', '}'];
|
||||
|
||||
export class BatchFileSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = [ 'REM', '::' ];
|
||||
public readonly commonCodeParts = [ ...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts ];
|
||||
public readonly commentDelimiters = ['REM', '::'];
|
||||
|
||||
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||
|
||||
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
|
||||
}
|
||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = [ '#' ];
|
||||
public readonly commonCodeParts = [ '(', ')', 'else', 'fi' ];
|
||||
public readonly commentDelimiters = ['#'];
|
||||
|
||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import { BatchFileSyntax } from './BatchFileSyntax';
|
||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||
|
||||
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
|
||||
}
|
||||
export class SyntaxFactory
|
||||
extends ScriptingLanguageFactory<ILanguageSyntax>
|
||||
implements ISyntaxFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,35 @@ import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compile
|
||||
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
|
||||
export class CodeSubstituter implements ICodeSubstituter {
|
||||
constructor(
|
||||
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
|
||||
private readonly date = new Date(),
|
||||
) {
|
||||
constructor(
|
||||
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
|
||||
private readonly date = new Date(),
|
||||
) {
|
||||
|
||||
}
|
||||
public substitute(code: string, info: IProjectInformation): string {
|
||||
if (!code) { throw new Error('undefined code'); }
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
const substitute = (name: string, value: string) =>
|
||||
args.addArgument(new FunctionCallArgument(name, value));
|
||||
substitute('homepage', info.homepage);
|
||||
substitute('version', info.version);
|
||||
substitute('date', this.date.toUTCString());
|
||||
const compiledCode = this.compiler.compileExpressions(code, args);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
public substitute(code: string, info: IProjectInformation): string {
|
||||
if (!code) { throw new Error('undefined code'); }
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
const substitute = (name: string, value: string) => args
|
||||
.addArgument(new FunctionCallArgument(name, value));
|
||||
substitute('homepage', info.homepage);
|
||||
substitute('version', info.version);
|
||||
substitute('date', this.date.toUTCString());
|
||||
const compiledCode = this.compiler.compileExpressions(code, args);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||
const parsers = [ new ParameterSubstitutionParser() ];
|
||||
const parser = new CompositeExpressionParser(parsers);
|
||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||
return expressionCompiler;
|
||||
const parsers = [new ParameterSubstitutionParser()];
|
||||
const parser = new CompositeExpressionParser(parsers);
|
||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||
return expressionCompiler;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
|
||||
export interface ICodeSubstituter {
|
||||
substitute(code: string, info: IProjectInformation): string;
|
||||
substitute(code: string, info: IProjectInformation): string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
@@ -8,24 +8,25 @@ import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
import { CodeSubstituter } from './CodeSubstituter';
|
||||
|
||||
export class ScriptingDefinitionParser {
|
||||
constructor(
|
||||
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||
) {
|
||||
}
|
||||
public parse(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation): IScriptingDefinition {
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
if (!definition) { throw new Error('undefined definition'); }
|
||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
constructor(
|
||||
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||
) {
|
||||
}
|
||||
|
||||
public parse(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation,
|
||||
): IScriptingDefinition {
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
if (!definition) { throw new Error('undefined definition'); }
|
||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
96
src/application/collections/collection.yaml.d.ts
vendored
96
src/application/collections/collection.yaml.d.ts
vendored
@@ -1,64 +1,64 @@
|
||||
declare module 'js-yaml-loader!@/*' {
|
||||
export interface CollectionData {
|
||||
readonly os: string;
|
||||
readonly scripting: ScriptingDefinitionData;
|
||||
readonly actions: ReadonlyArray<CategoryData>;
|
||||
readonly functions?: ReadonlyArray<FunctionData>;
|
||||
}
|
||||
export interface CollectionData {
|
||||
readonly os: string;
|
||||
readonly scripting: ScriptingDefinitionData;
|
||||
readonly actions: ReadonlyArray<CategoryData>;
|
||||
readonly functions?: ReadonlyArray<FunctionData>;
|
||||
}
|
||||
|
||||
export interface CategoryData extends DocumentableData {
|
||||
readonly children: ReadonlyArray<CategoryOrScriptData>;
|
||||
readonly category: string;
|
||||
}
|
||||
export interface CategoryData extends DocumentableData {
|
||||
readonly children: ReadonlyArray<CategoryOrScriptData>;
|
||||
readonly category: string;
|
||||
}
|
||||
|
||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
||||
|
||||
export interface DocumentableData {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
}
|
||||
export interface DocumentableData {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
}
|
||||
|
||||
export interface InstructionHolder {
|
||||
readonly name: string;
|
||||
export interface InstructionHolder {
|
||||
readonly name: string;
|
||||
|
||||
readonly code?: string;
|
||||
readonly revertCode?: string;
|
||||
readonly code?: string;
|
||||
readonly revertCode?: string;
|
||||
|
||||
readonly call?: FunctionCallsData;
|
||||
}
|
||||
readonly call?: FunctionCallsData;
|
||||
}
|
||||
|
||||
export interface ParameterDefinitionData {
|
||||
readonly name: string;
|
||||
readonly optional?: boolean;
|
||||
}
|
||||
export interface ParameterDefinitionData {
|
||||
readonly name: string;
|
||||
readonly optional?: boolean;
|
||||
}
|
||||
|
||||
export interface FunctionData extends InstructionHolder {
|
||||
readonly parameters?: readonly ParameterDefinitionData[];
|
||||
}
|
||||
export interface FunctionData extends InstructionHolder {
|
||||
readonly parameters?: readonly ParameterDefinitionData[];
|
||||
}
|
||||
|
||||
export interface FunctionCallParametersData {
|
||||
readonly [index: string]: string;
|
||||
}
|
||||
export interface FunctionCallParametersData {
|
||||
readonly [index: string]: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallData {
|
||||
readonly function: string;
|
||||
readonly parameters?: FunctionCallParametersData;
|
||||
}
|
||||
export interface FunctionCallData {
|
||||
readonly function: string;
|
||||
readonly parameters?: FunctionCallParametersData;
|
||||
}
|
||||
|
||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
|
||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||
readonly name: string;
|
||||
readonly recommend?: string;
|
||||
}
|
||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||
readonly name: string;
|
||||
readonly recommend?: string;
|
||||
}
|
||||
|
||||
export interface ScriptingDefinitionData {
|
||||
readonly language: string;
|
||||
readonly fileExtension: string;
|
||||
readonly startCode: string;
|
||||
readonly endCode: string;
|
||||
}
|
||||
export interface ScriptingDefinitionData {
|
||||
readonly language: string;
|
||||
readonly fileExtension: string;
|
||||
readonly startCode: string;
|
||||
readonly endCode: string;
|
||||
}
|
||||
|
||||
const content: CollectionData;
|
||||
export default content;
|
||||
const content: CollectionData;
|
||||
export default content;
|
||||
}
|
||||
|
||||
@@ -955,7 +955,7 @@ actions:
|
||||
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
|
||||
echo "Disabled Gatekeeper"
|
||||
else
|
||||
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
||||
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
||||
fi
|
||||
fi
|
||||
revertCode: |-
|
||||
@@ -974,7 +974,7 @@ actions:
|
||||
elif [ $gatekeeper_status = "enabled" ]; then
|
||||
echo "No action needed, Gatekeeper is already enabled"
|
||||
else
|
||||
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
||||
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
||||
fi
|
||||
fi
|
||||
-
|
||||
|
||||
Reference in New Issue
Block a user