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:
undergroundwires
2022-01-02 18:20:14 +01:00
parent 96265b75de
commit 5b1fbe1e2f
341 changed files with 16126 additions and 15101 deletions

View File

@@ -1,3 +1,5 @@
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
@@ -22,91 +24,10 @@ module.exports = {
ecmaVersion: 'latest', ecmaVersion: 'latest',
}, },
rules: { rules: {
'no-console': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off', ...getOwnRules(),
'no-debugger': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off', ...getTurnedOffBrokenRules(),
...getOpinionatedRuleOverrides(),
'linebreak-style': 'off', ...getTodoRules(),
'no-useless-constructor': 'off',
'import/prefer-default-export': 'off',
'class-methods-use-this': 'off',
'no-use-before-define': 'off',
'no-restricted-syntax': 'off',
'global-require': 'off',
'max-len': 'off',
'import/no-unresolved': 'off',
'import/no-webpack-loader-syntax': 'off',
'import/extensions': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-plusplus': 'off',
'no-mixed-operators': 'off',
'import/no-extraneous-dependencies': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-return-assign': 'off',
'no-await-in-loop': 'off',
'no-shadow': 'off',
'vuejs-accessibility/accessible-emoji': 'off',
'no-promise-executor-return': 'off',
'no-new': 'off',
'no-useless-escape': 'off',
'prefer-destructuring': 'off',
'no-param-reassign': 'off',
'no-irregular-whitespace': 'off',
'no-undef': 'off',
'no-underscore-dangle': 'off',
'vuejs-accessibility/form-control-has-label': 'off',
'vuejs-accessibility/click-events-have-key-events': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'camelcase': 'off',
'no-restricted-globals': 'off',
'default-param-last': 'off',
'no-continue': 'off',
'vuejs-accessibility/anchor-has-content': 'off',
'@typescript-eslint/no-extra-semi': 'off',
'no-multi-spaces': 'off',
'indent': 'off',
'comma-dangle': 'off',
'semi': 'off',
'quotes': 'off',
'key-spacing': 'off',
'lines-between-class-members': 'off',
'import/order': 'off',
'space-in-parens': 'off',
'array-bracket-spacing': 'off',
'object-curly-spacing': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'import/no-duplicates': 'off',
'function-paren-newline': 'off',
'operator-linebreak': 'off',
'no-multiple-empty-lines': 'off',
'object-curly-newline': 'off',
'object-property-newline': 'off',
'arrow-body-style': 'off',
'no-useless-return': 'off',
'prefer-template': 'off',
'func-call-spacing': 'off',
'no-spaced-func': 'off',
'padded-blocks': 'off',
'implicit-arrow-linebreak': 'off',
'function-call-argument-newline': 'off',
'comma-spacing': 'off',
'comma-style': 'off',
'newline-per-chained-call': 'off',
'no-useless-computed-key': 'off',
'no-else-return': 'off',
'quote-props': 'off',
'no-restricted-properties': 'off',
'prefer-exponentiation-operator': 'off',
'semi-spacing': 'off',
'prefer-object-spread': 'off',
'import/newline-after-import': 'off',
'strict': 'off',
'no-trailing-spaces': 'off',
'no-confusing-arrow': 'off',
'eol-last': 'off',
'import/no-useless-path-segments': 'off',
'spaced-comment': 'off',
'@typescript-eslint/no-empty-interface': 'off',
}, },
overrides: [ overrides: [
{ {
@@ -118,5 +39,77 @@ module.exports = {
mocha: true, mocha: true,
}, },
}, },
{
files: ['**/tests/**/*.{j,t}s?(x)'],
rules: {
'no-console': 'off',
},
},
], ],
}; };
function getOwnRules() {
return {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'linebreak-style': ['error', 'unix'], // Also enforced in .editorconfig
'import/order': [ // Enforce strict import order taking account into aliases
'error',
{
groups: [ // Enforce more strict order than AirBnb
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
'@/**', // @/..
'@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well)
'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/..
].map((pattern) => ({ pattern, group: 'internal' })),
},
],
};
}
function getTodoRules() { // Should be worked on separate future commits
return {
'import/no-extraneous-dependencies': 'off',
// Requires webpack configuration change with import '..yaml' files.
'import/no-webpack-loader-syntax': 'off',
'import/extensions': 'off',
'import/no-unresolved': 'off',
// Accessibility improvements:
'vuejs-accessibility/form-control-has-label': 'off',
'vuejs-accessibility/click-events-have-key-events': 'off',
'vuejs-accessibility/anchor-has-content': 'off',
'vuejs-accessibility/accessible-emoji': 'off',
};
}
function getTurnedOffBrokenRules() {
return {
// Broken in TypeScript
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
'no-shadow': 'off', // Fails with TypeScript enums
};
}
function getOpinionatedRuleOverrides() {
return {
// https://erkinekici.com/articles/linting-trap#no-use-before-define
'no-use-before-define': 'off',
// https://erkinekici.com/articles/linting-trap#arrow-body-style
'arrow-body-style': 'off',
// https://erkinekici.com/articles/linting-trap#no-plusplus
'no-plusplus': 'off',
// https://erkinekici.com/articles/linting-trap#no-param-reassign
'no-param-reassign': 'off',
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
'class-methods-use-this': 'off',
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
'import/prefer-default-export': 'off',
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
'no-restricted-syntax': [
baseStyleRules['no-restricted-syntax'][0],
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
],
};
}

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
presets: [ presets: [
'@vue/cli-plugin-babel/preset' '@vue/cli-plugin-babel/preset',
] ],
} };

View File

@@ -15,7 +15,7 @@
"lint:md": "markdownlint **/*.md --ignore node_modules", "lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent", "lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links", "lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:eslint": "vue-cli-service lint --no-fix", "lint:eslint": "vue-cli-service lint --no-fix --mode production",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps",

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: { plugins: {
autoprefixer: {} autoprefixer: {},
} },
} };

View File

@@ -7,15 +7,18 @@ export type ApplicationGetter = () => IApplication;
const ApplicationGetter: ApplicationGetter = parseApplication; const ApplicationGetter: ApplicationGetter = parseApplication;
export class ApplicationFactory implements IApplicationFactory { export class ApplicationFactory implements IApplicationFactory {
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) { private readonly getter: AsyncLazy<IApplication>;
if (!costlyGetter) {
throw new Error('undefined getter'); protected constructor(costlyGetter: ApplicationGetter) {
} if (!costlyGetter) {
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); throw new Error('undefined getter');
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
}
} }

View File

@@ -1,21 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order // Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) { export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); } if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); } if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1); const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2); const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2); return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) { function sort(array: readonly T[]) {
return array.slice().sort(); return array.slice().sort();
} }
} }
// Compares to Array<T> objects for equality in same order // Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) { export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); } if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); } if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) { if (array1.length !== array2.length) {
return false; return false;
} }
return array1.every((val, index) => val === array2[index]); return array1.every((val, index) => val === array2[index]);
} }

View File

@@ -1,54 +1,63 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
export type EnumType = number | string; 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> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): 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];
} }
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): string[] { enumVariable: EnumVariable<T, TEnumValue>,
return Object ): IEnumParser<TEnumValue> {
.values(enumVariable) return {
.filter((enumMember) => typeof enumMember === 'string') as string[]; 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>( export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] { enumVariable: EnumVariable<T, TEnumValue>,
return getEnumNames(enumVariable) ): TEnumValue[] {
.map((level) => enumVariable[level]) as TEnumValue[]; return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[];
} }
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>( export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue, value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) { enumVariable: EnumVariable<T, TEnumValue>,
if (value === undefined) { ) {
throw new Error('undefined enum value'); if (value === undefined) {
} throw new Error('undefined enum value');
if (!(value in enumVariable)) { }
throw new RangeError(`enum value "${value}" is out of range`); if (!(value in enumVariable)) {
} throw new RangeError(`enum value "${value}" is out of range`);
}
} }

View File

@@ -1,5 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> { export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T; create(language: ScriptingLanguage): T;
} }

View File

@@ -1,31 +1,30 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
type Getter<T> = () => T; type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<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 { public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) { if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
} }
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) { protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage); assertInRange(language, ScriptingLanguage);
if (!getter) { if (!getter) {
throw new Error('undefined getter'); throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
} }
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
} }

View File

@@ -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 { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; 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>; type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext { export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>(); public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection;
public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState { public collection: ICategoryCollection;
return this.states[this.collection.os];
}
private readonly states: StateMachine; public currentOs: OperatingSystem;
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 { public get state(): ICategoryCollectionState {
if (this.currentOs === os) { return this.states[this.collection.os];
return; }
}
this.collection = this.app.getCollection(os); private readonly states: StateMachine;
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); public constructor(
} public readonly app: IApplication,
const event: IApplicationContextChangedEvent = { initialContext: OperatingSystem,
newState: this.states[os], ) {
oldState: this.states[this.currentOs], validateApp(app);
}; assertInRange(initialContext, OperatingSystem);
this.contextChanged.notify(event); this.states = initializeStates(app);
this.currentOs = os; 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) { function validateApp(app: IApplication) {
if (!app) { if (!app) {
throw new Error('undefined app'); throw new Error('undefined app');
} }
} }
function initializeStates(app: IApplication): StateMachine { function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>(); const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) { for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection); machine[collection.os] = new CategoryCollectionState(collection);
} }
return machine; return machine;
} }

View File

@@ -1,31 +1,32 @@
import { ApplicationContext } from './ApplicationContext';
import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { Environment } from '../Environment/Environment';
import { IEnvironment } from '../Environment/IEnvironment'; import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory'; import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory';
import { ApplicationContext } from './ApplicationContext';
export async function buildContext( export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current, factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> { environment = Environment.CurrentEnvironment,
if (!factory) { throw new Error('undefined factory'); } ): Promise<IApplicationContext> {
if (!environment) { throw new Error('undefined environment'); } if (!factory) { throw new Error('undefined factory'); }
const app = await factory.getApp(); if (!environment) { throw new Error('undefined environment'); }
const os = getInitialOs(app, environment); const app = await factory.getApp();
return new ApplicationContext(app, os); const os = getInitialOs(app, environment);
return new ApplicationContext(app, os);
} }
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem { function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
const currentOs = environment.os; const currentOs = environment.os;
const supportedOsList = app.getSupportedOsList(); const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) { if (supportedOsList.includes(currentOs)) {
return currentOs; return currentOs;
} }
supportedOsList.sort((os1, os2) => { supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1); return getPriority(os2) - getPriority(os1);
}); });
return supportedOsList[0]; return supportedOsList[0];
} }

View File

@@ -1,12 +1,12 @@
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
export interface IReadOnlyApplicationContext { export interface IReadOnlyApplicationContext {
readonly app: IApplication; readonly app: IApplication;
readonly state: IReadOnlyCategoryCollectionState; readonly state: IReadOnlyCategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>; readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
} }
export interface IApplicationContext extends IReadOnlyApplicationContext { export interface IApplicationContext extends IReadOnlyApplicationContext {
@@ -15,6 +15,6 @@ export interface IApplicationContext extends IReadOnlyApplicationContext {
} }
export interface IApplicationContextChangedEvent { export interface IApplicationContextChangedEvent {
readonly newState: ICategoryCollectionState; readonly newState: ICategoryCollectionState;
readonly oldState: ICategoryCollectionState; readonly oldState: ICategoryCollectionState;
} }

View File

@@ -1,3 +1,5 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { UserFilter } from './Filter/UserFilter'; import { UserFilter } from './Filter/UserFilter';
import { IUserFilter } from './Filter/IUserFilter'; import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';
@@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection'; import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState'; import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class CategoryCollectionState implements ICategoryCollectionState { export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem; public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly filter: IUserFilter;
public constructor(readonly collection: ICategoryCollection) { public readonly code: IApplicationCode;
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting); public readonly selection: IUserSelection;
this.filter = new UserFilter(collection);
this.os = collection.os; 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;
}
} }

View File

@@ -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 { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition'; import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; 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 { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new EventSource<ICodeChangedEvent>(); public readonly changed = new EventSource<ICodeChangedEvent>();
public current: string;
private scriptPositions = new Map<SelectedScript, CodePosition>(); public current: string;
constructor( private scriptPositions = new Map<SelectedScript, CodePosition>();
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 { constructor(
const oldScripts = Array.from(this.scriptPositions.keys()); userSelection: IReadOnlyUserSelection,
const code = this.generator.buildCode(scripts, this.scriptingDefinition); private readonly scriptingDefinition: IScriptingDefinition,
this.current = code.code; private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
this.scriptPositions = code.scriptPositions; ) {
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions); if (!userSelection) { throw new Error('userSelection is null or undefined'); }
this.changed.notify(event); 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);
}
} }

View File

@@ -1,64 +1,72 @@
import { ICodeChangedEvent } from './ICodeChangedEvent';
import { SelectedScript } from '../../Selection/SelectedScript';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '../../Selection/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string; public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>;
private readonly scripts: Map<IScript, ICodePosition>; public readonly addedScripts: ReadonlyArray<IScript>;
constructor( public readonly removedScripts: ReadonlyArray<IScript>;
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 { public readonly changedScripts: ReadonlyArray<IScript>;
return this.scripts.size === 0;
}
public getScriptPositionInCode(script: IScript): ICodePosition { private readonly scripts: Map<IScript, ICodePosition>;
return this.scripts.get(script);
} 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>) { function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length; const totalLines = script.split(/\r\n|\r|\n/).length;
for (const position of positions) { for (const position of positions) {
if (position.endLine > totalLines) { if (position.endLine > totalLines) {
throw new Error(`script end line (${position.endLine}) is out of range.` + throw new Error(
`(total code lines: ${totalLines}`); `script end line (${position.endLine}) is out of range.`
} + `(total code lines: ${totalLines}`,
);
} }
}
} }
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> { newScripts: ReadonlyArray<SelectedScript>,
return newScripts ): ReadonlyArray<IScript> {
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id return newScripts
&& oldScript.revert !== newScript.revert )) .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
.map((selection) => selection.script); && oldScript.revert !== newScript.revert))
.map((selection) => selection.script);
} }
function selectIfNotExists( function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>, selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>) { test: ReadonlyArray<SelectedScript>,
return selectableContainer ) {
.filter((script) => !test.find((oldScript) => oldScript.id === script.id)) return selectableContainer
.map((selection) => selection.script); .filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script);
} }

View File

@@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent { export interface ICodeChangedEvent {
readonly code: string; readonly code: string;
addedScripts: ReadonlyArray<IScript>; addedScripts: ReadonlyArray<IScript>;
removedScripts: ReadonlyArray<IScript>; removedScripts: ReadonlyArray<IScript>;
changedScripts: ReadonlyArray<IScript>; changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition; getScriptPositionInCode(script: IScript): ICodePosition;
} }

View File

@@ -4,64 +4,67 @@ const NewLine = '\n';
const TotalFunctionSeparatorChars = 58; const TotalFunctionSeparatorChars = 58;
export abstract class CodeBuilder implements ICodeBuilder { 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) // Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number { public get currentLine(): number {
return this.lines.length; return this.lines.length;
}
public appendLine(code?: string): CodeBuilder {
if (!code) {
this.lines.push('');
return this;
} }
const lines = code.match(/[^\r\n]+/g);
public appendLine(code?: string): CodeBuilder { for (const line of lines) {
if (!code) { this.lines.push(line);
this.lines.push('');
return this;
}
const lines = code.match(/[^\r\n]+/g);
for (const line of lines) {
this.lines.push(line);
}
return this;
} }
return this;
}
public appendTrailingHyphensCommentLine( public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { totalRepeatHyphens: number = TotalFunctionSeparatorChars,
return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); ): 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 { public toString(): string {
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`); return this.lines.join(NewLine);
return this; }
}
public appendFunction(name: string, code: string): CodeBuilder { protected abstract getCommentDelimiter(): string;
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( protected abstract writeStandardOut(text: string): string;
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;
} }

View File

@@ -5,10 +5,12 @@ import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder'; import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory'; import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory { export class CodeBuilderFactory
constructor() { extends ScriptingLanguageFactory<ICodeBuilder>
super(); implements ICodeBuilderFactory {
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); constructor() {
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder()); super();
} this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
}
} }

View File

@@ -1,9 +1,9 @@
export interface ICodeBuilder { export interface ICodeBuilder {
currentLine: number; currentLine: number;
appendLine(code?: string): ICodeBuilder; appendLine(code?: string): ICodeBuilder;
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder; appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
appendCommentLine(commentLine?: string): ICodeBuilder; appendCommentLine(commentLine?: string): ICodeBuilder;
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder; appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
appendFunction(name: string, code: string): ICodeBuilder; appendFunction(name: string, code: string): ICodeBuilder;
toString(): string; toString(): string;
} }

View File

@@ -1,5 +1,4 @@
import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ICodeBuilder } from './ICodeBuilder';
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> { export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
}

View File

@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface IUserScript { export interface IUserScript {
code: string; code: string;
scriptPositions: Map<SelectedScript, ICodePosition>; scriptPositions: Map<SelectedScript, ICodePosition>;
} }

View File

@@ -1,9 +1,9 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { IUserScript } from './IUserScript';
export interface IUserScriptGenerator { export interface IUserScriptGenerator {
buildCode( buildCode(
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript; scriptingDefinition: IScriptingDefinition): IUserScript;
} }

View File

@@ -1,16 +1,17 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class BatchBuilder extends CodeBuilder { export class BatchBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '::'; return '::';
} }
protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`; protected writeStandardOut(text: string): string {
} return `echo ${escapeForEcho(text)}`;
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {
return text return text
.replace(/&/g, '^&') .replace(/&/g, '^&')
.replace(/%/g, '%%'); .replace(/%/g, '%%');
} }

View File

@@ -1,15 +1,16 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class ShellBuilder extends CodeBuilder { export class ShellBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '#'; return '#';
} }
protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`; protected writeStandardOut(text: string): string {
} return `echo '${escapeForEcho(text)}'`;
}
} }
function escapeForEcho(text: string) { function escapeForEcho(text: string) {
return text return text
.replace(/'/g, '\'\\\'\''); .replace(/'/g, '\'\\\'\'');
} }

View File

@@ -1,71 +1,76 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScript } from './IUserScript';
import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory'; import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { CodeBuilderFactory } from './CodeBuilderFactory'; import { CodeBuilderFactory } from './CodeBuilderFactory';
export class UserScriptGenerator implements IUserScriptGenerator { 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( let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
selectedScripts: ReadonlyArray<SelectedScript>, builder = initializeCode(scriptingDefinition.startCode, builder);
scriptingDefinition: IScriptingDefinition): IUserScript { for (const selection of selectedScripts) {
if (!selectedScripts) { throw new Error('undefined scripts'); } scriptPositions = appendSelection(selection, scriptPositions, builder);
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 };
} }
const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions };
}
} }
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder { function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
if (!startCode) { if (!startCode) {
return builder; return builder;
} }
return builder return builder
.appendLine(startCode) .appendLine(startCode)
.appendLine(); .appendLine();
} }
function finalizeCode(builder: ICodeBuilder, endCode: string): string { function finalizeCode(builder: ICodeBuilder, endCode: string): string {
if (!endCode) { if (!endCode) {
return builder.toString(); return builder.toString();
} }
return builder.appendLine() return builder.appendLine()
.appendLine(endCode) .appendLine(endCode)
.toString(); .toString();
} }
function appendSelection( function appendSelection(
selection: SelectedScript, selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>, scriptPositions: Map<SelectedScript, ICodePosition>,
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> { builder: ICodeBuilder,
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts ): Map<SelectedScript, ICodePosition> {
builder = appendCode(selection, builder); // Start from next line because first line will be empty to separate scripts
const endPosition = builder.currentLine - 1; const startPosition = builder.currentLine + 1;
builder.appendLine(); appendCode(selection, builder);
const position = new CodePosition(startPosition, endPosition); const endPosition = builder.currentLine - 1;
scriptPositions.set(selection, position); builder.appendLine();
return scriptPositions; const position = new CodePosition(startPosition, endPosition);
scriptPositions.set(selection, position);
return scriptPositions;
} }
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name; const { script } = selection;
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute; const name = selection.revert ? `${script.name} (revert)` : script.name;
return builder const scriptCode = selection.revert ? script.code.revert : script.code.execute;
.appendLine() return builder
.appendFunction(name, scriptCode); .appendLine()
.appendFunction(name, scriptCode);
} }

View File

@@ -1,7 +1,7 @@
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
export interface IApplicationCode { export interface IApplicationCode {
readonly changed: IEventSource<ICodeChangedEvent>; readonly changed: IEventSource<ICodeChangedEvent>;
readonly current: string; readonly current: string;
} }

View File

@@ -1,24 +1,25 @@
import { ICodePosition } from './ICodePosition'; import { ICodePosition } from './ICodePosition';
export class CodePosition implements ICodePosition { export class CodePosition implements ICodePosition {
public get totalLines(): number { public get totalLines(): number {
return this.endLine - this.startLine; return this.endLine - this.startLine;
} }
constructor( constructor(
public readonly startLine: number, public readonly startLine: number,
public readonly endLine: number) { public readonly endLine: number,
if (startLine < 0) { ) {
throw new Error('Code cannot start in a negative line'); 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');
}
} }
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');
}
}
} }

View File

@@ -1,5 +1,5 @@
export interface ICodePosition { export interface ICodePosition {
readonly startLine: number; readonly startLine: number;
readonly endLine: number; readonly endLine: number;
readonly totalLines: number; readonly totalLines: number;
} }

View File

@@ -1,18 +1,20 @@
import { IFilterResult } from './IFilterResult';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IFilterResult } from './IFilterResult';
export class FilterResult implements IFilterResult { export class FilterResult implements IFilterResult {
constructor( constructor(
public readonly scriptMatches: ReadonlyArray<IScript>, public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>, public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly query: string) { public readonly query: string,
if (!query) { throw new Error('Query is empty or undefined'); } ) {
if (!scriptMatches) { throw new Error('Script matches is undefined'); } if (!query) { throw new Error('Query is empty or undefined'); }
if (!categoryMatches) { throw new Error('Category matches is 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; public hasAnyMatches(): boolean {
} return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0;
}
} }

View File

@@ -1,8 +1,8 @@
import { IScript, ICategory } from '@/domain/ICategory'; import { IScript, ICategory } from '@/domain/ICategory';
export interface IFilterResult { export interface IFilterResult {
readonly categoryMatches: ReadonlyArray<ICategory>; readonly categoryMatches: ReadonlyArray<ICategory>;
readonly scriptMatches: ReadonlyArray<IScript>; readonly scriptMatches: ReadonlyArray<IScript>;
readonly query: string; readonly query: string;
hasAnyMatches(): boolean; hasAnyMatches(): boolean;
} }

View File

@@ -2,12 +2,12 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
export interface IReadOnlyUserFilter { export interface IReadOnlyUserFilter {
readonly currentFilter: IFilterResult | undefined; readonly currentFilter: IFilterResult | undefined;
readonly filtered: IEventSource<IFilterResult>; readonly filtered: IEventSource<IFilterResult>;
readonly filterRemoved: IEventSource<void>; readonly filterRemoved: IEventSource<void>;
} }
export interface IUserFilter extends IReadOnlyUserFilter { export interface IUserFilter extends IReadOnlyUserFilter {
setFilter(filter: string): void; setFilter(filter: string): void;
removeFilter(): void; removeFilter(): void;
} }

View File

@@ -1,52 +1,56 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { FilterResult } from './FilterResult'; import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { IUserFilter } from './IUserFilter'; import { IUserFilter } from './IUserFilter';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserFilter implements IUserFilter { export class UserFilter implements IUserFilter {
public readonly filtered = new EventSource<IFilterResult>(); public readonly filtered = new EventSource<IFilterResult>();
public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined;
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 { public removeFilter(): void {
if (!filter) { this.currentFilter = undefined;
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter'); this.filterRemoved.notify();
} }
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();
}
} }
function isScriptAMatch(script: IScript, filterLowercase: string) { function isScriptAMatch(script: IScript, filterLowercase: string) {
if (script.name.toLowerCase().includes(filterLowercase)) { if (script.name.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.code.execute.toLowerCase().includes(filterLowercase)) { if (script.code.execute.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.code.revert) { if (script.code.revert) {
return script.code.revert.toLowerCase().includes(filterLowercase); return script.code.revert.toLowerCase().includes(filterLowercase);
} }
return false; return false;
} }

View File

@@ -1,18 +1,18 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter'; import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection'; import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IReadOnlyCategoryCollectionState { export interface IReadOnlyCategoryCollectionState {
readonly code: IApplicationCode; readonly code: IApplicationCode;
readonly os: OperatingSystem; readonly os: OperatingSystem;
readonly filter: IReadOnlyUserFilter; readonly filter: IReadOnlyUserFilter;
readonly selection: IReadOnlyUserSelection; readonly selection: IReadOnlyUserSelection;
readonly collection: ICategoryCollection; readonly collection: ICategoryCollection;
} }
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
readonly filter: IUserFilter; readonly filter: IUserFilter;
readonly selection: IUserSelection; readonly selection: IUserSelection;
} }

View File

@@ -1,23 +1,23 @@
import { SelectedScript } from './SelectedScript';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { SelectedScript } from './SelectedScript';
export interface IReadOnlyUserSelection { export interface IReadOnlyUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>; readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
isSelected(scriptId: string): boolean; isSelected(scriptId: string): boolean;
areAllSelected(category: ICategory): boolean; areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean; isAnySelected(category: ICategory): boolean;
} }
export interface IUserSelection extends IReadOnlyUserSelection { export interface IUserSelection extends IReadOnlyUserSelection {
removeAllInCategory(categoryId: number): void; removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void; addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void; addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void; addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void; removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void; selectOnly(scripts: ReadonlyArray<IScript>): void;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
} }

View File

@@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
export class SelectedScript extends BaseEntity<string> { export class SelectedScript extends BaseEntity<string> {
constructor( constructor(
public readonly script: IScript, public readonly script: IScript,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); super(script.id);
if (revert && !script.canRevert()) { if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script'); throw new Error('cannot revert an irreversible script');
}
} }
}
} }

View File

@@ -1,141 +1,145 @@
import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository'; import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>;
constructor( private readonly scripts: IRepository<string, SelectedScript>;
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 areAllSelected(category: ICategory): boolean { constructor(
if (this.selectedScripts.length === 0) { private readonly collection: ICategoryCollection,
return false; selectedScripts: ReadonlyArray<SelectedScript>,
} ) {
const scripts = category.getAllScriptsRecursively(); this.scripts = new InMemoryRepository<string, SelectedScript>();
if (this.selectedScripts.length < scripts.length) { if (selectedScripts && selectedScripts.length > 0) {
return false; for (const script of selectedScripts) {
} this.scripts.addItem(script);
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id)); }
} }
}
public isAnySelected(category: ICategory): boolean { public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) { if (this.selectedScripts.length === 0) {
return false; return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
} }
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 { public isAnySelected(category: ICategory): boolean {
const category = this.collection.findCategory(categoryId); if (this.selectedScripts.length === 0) {
const scriptsToRemove = category.getAllScriptsRecursively() return false;
.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());
} }
return this.selectedScripts.some((s) => category.includes(s.script));
}
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void { public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId); const category = this.collection.findCategory(categoryId);
const scriptsToAddOrUpdate = category.getAllScriptsRecursively() const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => .filter((script) => this.scripts.exists(script.id));
!this.scripts.exists(script.id) if (!scriptsToRemove.length) {
|| this.scripts.getById(script.id).revert !== revert, return;
);
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());
} }
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void { public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const script = this.collection.findScript(scriptId); const category = this.collection.findCategory(categoryId);
if (!script) { const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); .filter(
} (script) => !this.scripts.exists(script.id)
const selectedScript = new SelectedScript(script, revert); || this.scripts.getById(script.id).revert !== revert,
this.scripts.addItem(selectedScript); );
this.changed.notify(this.scripts.getItems()); 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 { public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId); const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert); if (!script) {
this.scripts.addOrUpdateItem(selectedScript); throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
this.changed.notify(this.scripts.getItems());
} }
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public removeSelectedScript(scriptId: string): void { public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
this.scripts.removeItem(scriptId); const script = this.collection.findScript(scriptId);
this.changed.notify(this.scripts.getItems()); const selectedScript = new SelectedScript(script, revert);
} this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public isSelected(scriptId: string): boolean { public removeSelectedScript(scriptId: string): void {
return this.scripts.exists(scriptId); this.scripts.removeItem(scriptId);
} this.changed.notify(this.scripts.getItems());
}
/** Get users scripts based on his/her selections */ public isSelected(scriptId: string): boolean {
public get selectedScripts(): ReadonlyArray<SelectedScript> { return this.scripts.exists(scriptId);
return this.scripts.getItems(); }
}
public selectAll(): void { /** Get users scripts based on his/her selections */
for (const script of this.collection.getAllScripts()) { public get selectedScripts(): ReadonlyArray<SelectedScript> {
if (!this.scripts.exists(script.id)) { return this.scripts.getItems();
const selection = new SelectedScript(script, false); }
this.scripts.addItem(selection);
}
}
this.changed.notify(this.scripts.getItems());
}
public deselectAll(): void { public selectAll(): void {
const selectedScriptIds = this.scripts.getItems().map((script) => script.id); for (const script of this.collection.getAllScripts()) {
for (const scriptId of selectedScriptIds) { if (!this.scripts.exists(script.id)) {
this.scripts.removeItem(scriptId); const selection = new SelectedScript(script, false);
} this.scripts.addItem(selection);
this.changed.notify([]); }
} }
this.changed.notify(this.scripts.getItems());
}
public selectOnly(scripts: readonly IScript[]): void { public deselectAll(): void {
if (!scripts || scripts.length === 0) { const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything'); for (const scriptId of selectedScriptIds) {
} this.scripts.removeItem(scriptId);
// 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());
} }
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());
}
} }

View File

@@ -3,52 +3,55 @@ import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector { export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors; private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) { public detect(userAgent: string): OperatingSystem | undefined {
return undefined; if (!userAgent) {
} return undefined;
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== undefined) {
return os;
}
}
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 // Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors = const BrowserDetectors = [
[ define(OperatingSystem.KaiOS, (b) => b
define(OperatingSystem.KaiOS, (b) => .mustInclude('KAIOS')),
b.mustInclude('KAIOS')), define(OperatingSystem.ChromeOS, (b) => b
define(OperatingSystem.ChromeOS, (b) => .mustInclude('CrOS')),
b.mustInclude('CrOS')), define(OperatingSystem.BlackBerryOS, (b) => b
define(OperatingSystem.BlackBerryOS, (b) => .mustInclude('BlackBerry')),
b.mustInclude('BlackBerry')), define(OperatingSystem.BlackBerryTabletOS, (b) => b
define(OperatingSystem.BlackBerryTabletOS, (b) => .mustInclude('RIM Tablet OS')),
b.mustInclude('RIM Tablet OS')), define(OperatingSystem.BlackBerry, (b) => b
define(OperatingSystem.BlackBerry, (b) => .mustInclude('BB10')),
b.mustInclude('BB10')), define(OperatingSystem.Android, (b) => b
define(OperatingSystem.Android, (b) => .mustInclude('Android').mustNotInclude('Windows Phone')),
b.mustInclude('Android').mustNotInclude('Windows Phone')), define(OperatingSystem.Android, (b) => b
define(OperatingSystem.Android, (b) => .mustInclude('Adr').mustNotInclude('Windows Phone')),
b.mustInclude('Adr').mustNotInclude('Windows Phone')), define(OperatingSystem.iOS, (b) => b
define(OperatingSystem.iOS, (b) => .mustInclude('like Mac OS X')),
b.mustInclude('like Mac OS X')), define(OperatingSystem.Linux, (b) => b
define(OperatingSystem.Linux, (b) => .mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')), define(OperatingSystem.Windows, (b) => b
define(OperatingSystem.Windows, (b) => .mustInclude('Windows').mustNotInclude('Windows Phone')),
b.mustInclude('Windows').mustNotInclude('Windows Phone')), define(OperatingSystem.WindowsPhone, (b) => b
define(OperatingSystem.WindowsPhone, (b) => .mustInclude('Windows Phone')),
b.mustInclude('Windows Phone')), define(OperatingSystem.macOS, (b) => b
define(OperatingSystem.macOS, (b) => .mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
]; ];
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector { function define(
const builder = new DetectorBuilder(os); os: OperatingSystem,
applyRules(builder); applyRules: (builder: DetectorBuilder) => DetectorBuilder,
return builder.build(); ): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
} }

View File

@@ -1,53 +1,54 @@
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class DetectorBuilder { export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>(); private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { } private readonly notExistingPartsInUserAgent = new Array<string>();
public mustInclude(str: string): DetectorBuilder { constructor(private readonly os: OperatingSystem) { }
return this.add(str, this.existingPartsInUserAgent);
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 { private detect(userAgent: string): OperatingSystem {
return this.add(str, this.notExistingPartsInUserAgent); 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 { private add(part: string, array: string[]): DetectorBuilder {
if (!this.existingPartsInUserAgent.length) { if (!part) {
throw new Error('Must include at least a part'); throw new Error('part is empty or undefined');
}
return {
detect: (agent) => this.detect(agent),
};
} }
if (this.existingPartsInUserAgent.includes(part)) {
private detect(userAgent: string): OperatingSystem { throw new Error(`part ${part} is already included as existing part`);
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.notExistingPartsInUserAgent.includes(part)) {
private add(part: string, array: string[]): DetectorBuilder { throw new Error(`part ${part} is already included as not existing part`);
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;
} }
array.push(part);
return this;
}
} }

View File

@@ -1,5 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector { export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem | undefined; detect(userAgent: string): OperatingSystem | undefined;
} }

View File

@@ -1,83 +1,89 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment'; import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
interface IEnvironmentVariables { export interface IEnvironmentVariables {
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process; readonly process: NodeJS.Process;
readonly navigator: Navigator; readonly navigator: Navigator;
} }
export class Environment implements IEnvironment { export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({ public static readonly CurrentEnvironment: IEnvironment = new Environment({
window, window,
process: typeof process !== 'undefined' ? process /* electron only */ : undefined, process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
navigator, navigator,
}); });
public readonly isDesktop: boolean;
public readonly os: OperatingSystem; public readonly isDesktop: boolean;
protected constructor(
variables: IEnvironmentVariables, public readonly os: OperatingSystem;
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
if (!variables) { protected constructor(
throw new Error('variables is null or empty'); variables: IEnvironmentVariables,
} browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
this.isDesktop = isDesktop(variables); ) {
if (this.isDesktop) { if (!variables) {
this.os = getDesktopOsType(getProcessPlatform(variables)); throw new Error('variables is null or empty');
} else {
const userAgent = getUserAgent(variables);
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
}
} }
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 { function getUserAgent(variables: IEnvironmentVariables): string {
if (!variables.window || !variables.window.navigator) { if (!variables.window || !variables.window.navigator) {
return undefined; return undefined;
} }
return variables.window.navigator.userAgent; return variables.window.navigator.userAgent;
} }
function getProcessPlatform(variables: IEnvironmentVariables): string { function getProcessPlatform(variables: IEnvironmentVariables): string {
if (!variables.process || !variables.process.platform) { if (!variables.process || !variables.process.platform) {
return undefined; return undefined;
} }
return variables.process.platform; return variables.process.platform;
} }
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined { function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform // https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') { switch (processPlatform) {
return OperatingSystem.macOS; case 'darwin':
} else if (processPlatform === 'win32') { return OperatingSystem.macOS;
return OperatingSystem.Windows; case 'win32':
} else if (processPlatform === 'linux') { return OperatingSystem.Windows;
return OperatingSystem.Linux; case 'linux':
} return OperatingSystem.Linux;
return undefined; default:
return undefined;
}
} }
function isDesktop(variables: IEnvironmentVariables): boolean { function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288 // More: https://github.com/electron/electron/issues/2288
// Renderer process // Renderer process
if (variables.window if (variables.window
&& variables.window.process && variables.window.process
&& variables.window.process.type === 'renderer') { && variables.window.process.type === 'renderer') {
return true; return true;
} }
// Main process // Main process
if (variables.process if (variables.process
&& variables.process.versions && variables.process.versions
&& Boolean(variables.process.versions.electron)) { && Boolean(variables.process.versions.electron)) {
return true; return true;
} }
// Detect the user agent when the `nodeIntegration` option is set to true // Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator if (variables.navigator
&& variables.navigator.userAgent && variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) { && variables.navigator.userAgent.includes('Electron')) {
return true; return true;
} }
return false; return false;
} }

View File

@@ -1,6 +1,6 @@
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment { export interface IEnvironment {
readonly isDesktop: boolean; readonly isDesktop: boolean;
readonly os: OperatingSystem; readonly os: OperatingSystem;
} }

View File

@@ -1,5 +1,5 @@
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
export interface IApplicationFactory { export interface IApplicationFactory {
getApp(): Promise<IApplication>; getApp(): Promise<IApplication>;
} }

View File

@@ -1,38 +1,41 @@
import { CollectionData } from 'js-yaml-loader!@/*';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { parseCategoryCollection } from './CategoryCollectionParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml'; import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml'; import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
parser = CategoryCollectionParser, parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env, processEnv: NodeJS.ProcessEnv = process.env,
collectionsData = PreParsedCollections): IApplication { collectionsData = PreParsedCollections,
validateCollectionsData(collectionsData); ): IApplication {
const information = parseProjectInformation(processEnv); validateCollectionsData(collectionsData);
const collections = collectionsData.map((collection) => parser(collection, information)); const information = parseProjectInformation(processEnv);
const app = new Application(information, collections); const collections = collectionsData.map((collection) => parser(collection, information));
return app; const app = new Application(information, collections);
return app;
} }
export type CategoryCollectionParserType export type CategoryCollectionParserType
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection; = (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
const CategoryCollectionParser: CategoryCollectionParserType const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
= (file, info) => parseCategoryCollection(file, info); return parseCategoryCollection(file, info);
};
const PreParsedCollections: readonly CollectionData [] const PreParsedCollections: readonly CollectionData [] = [
= [ WindowsData, MacOsData ]; WindowsData, MacOsData,
];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections.length) { if (!collections.length) {
throw new Error('no collection provided'); throw new Error('no collection provided');
} }
if (collections.some((collection) => !collection)) { if (collections.some((collection) => !collection)) {
throw new Error('undefined collection provided'); throw new Error('undefined collection provided');
} }
} }

View File

@@ -1,40 +1,42 @@
import { Category } from '@/domain/Category';
import { CollectionData } from 'js-yaml-loader!@/*'; import { CollectionData } from 'js-yaml-loader!@/*';
import { parseCategory } from './CategoryParser'; import { Category } from '@/domain/Category';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { createEnumParser } from '../Common/Enum';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { createEnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
export function parseCategoryCollection( export function parseCategoryCollection(
content: CollectionData, content: CollectionData,
info: IProjectInformation, info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection { osParser = createEnumParser(OperatingSystem),
validate(content); ): ICategoryCollection {
const scripting = new ScriptingDefinitionParser() validate(content);
.parse(content.scripting, info); const scripting = new ScriptingDefinitionParser()
const context = new CategoryCollectionParseContext(content.functions, scripting); .parse(content.scripting, info);
const categories = new Array<Category>(); const context = new CategoryCollectionParseContext(content.functions, scripting);
for (const action of content.actions) { const categories = new Array<Category>();
const category = parseCategory(action, context); for (const action of content.actions) {
categories.push(category); const category = parseCategory(action, context);
} categories.push(category);
const os = osParser.parseEnum(content.os, 'os'); }
const collection = new CategoryCollection( const os = osParser.parseEnum(content.os, 'os');
os, const collection = new CategoryCollection(
categories, os,
scripting); categories,
return collection; scripting,
);
return collection;
} }
function validate(content: CollectionData): void { function validate(content: CollectionData): void {
if (!content) { if (!content) {
throw new Error('content is null or undefined'); throw new Error('content is null or undefined');
} }
if (!content.actions || content.actions.length <= 0) { if (!content.actions || content.actions.length <= 0) {
throw new Error('content does not define any action'); throw new Error('content does not define any action');
} }
} }

View File

@@ -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 { Script } from '@/domain/Script';
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser'; import { parseDocUrls } from './DocumentationParser';
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
import { parseScript } from './Script/ScriptParser'; import { parseScript } from './Script/ScriptParser';
let categoryIdCounter: number = 0; let categoryIdCounter = 0;
interface ICategoryChildren { interface ICategoryChildren {
subCategories: Category[]; subCategories: Category[];
subScripts: Script[]; subScripts: Script[];
} }
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category { export function parseCategory(
if (!context) { throw new Error('undefined context'); } category: CategoryData,
ensureValid(category); context: ICategoryCollectionParseContext,
const children: ICategoryChildren = { ): Category {
subCategories: new Array<Category>(), if (!context) { throw new Error('undefined context'); }
subScripts: new Array<Script>(), ensureValid(category);
}; const children: ICategoryChildren = {
for (const data of category.children) { subCategories: new Array<Category>(),
parseCategoryChild(data, children, category, context); subScripts: new Array<Script>(),
} };
return new Category( for (const data of category.children) {
/*id*/ categoryIdCounter++, parseCategoryChild(data, children, category, context);
/*name*/ category.category, }
/*docs*/ parseDocUrls(category), return new Category(
/*categories*/ children.subCategories, /* id: */ categoryIdCounter++,
/*scripts*/ children.subScripts, /* name: */ category.category,
); /* docs: */ parseDocUrls(category),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} }
function ensureValid(category: CategoryData) { function ensureValid(category: CategoryData) {
if (!category) { if (!category) {
throw Error('category is null or undefined'); throw Error('category is null or undefined');
} }
if (!category.children || category.children.length === 0) { if (!category.children || category.children.length === 0) {
throw Error(`category has no children: "${category.category}"`); throw Error(`category has no children: "${category.category}"`);
} }
if (!category.category || category.category.length === 0) { if (!category.category || category.category.length === 0) {
throw Error('category has no name'); throw Error('category has no name');
} }
} }
function parseCategoryChild( function parseCategoryChild(
data: CategoryOrScriptData, data: CategoryOrScriptData,
children: ICategoryChildren, children: ICategoryChildren,
parent: CategoryData, parent: CategoryData,
context: ICategoryCollectionParseContext) { context: ICategoryCollectionParseContext,
if (isCategory(data)) { ) {
const subCategory = parseCategory(data as CategoryData, context); if (isCategory(data)) {
children.subCategories.push(subCategory); const subCategory = parseCategory(data as CategoryData, context);
} else if (isScript(data)) { children.subCategories.push(subCategory);
const scriptData = data as ScriptData; } else if (isScript(data)) {
const script = parseScript(scriptData, context); const scriptData = data as ScriptData;
children.subScripts.push(script); const script = parseScript(scriptData, context);
} else { children.subScripts.push(script);
throw new Error(`Child element is neither a category or a script. } else {
throw new Error(`Child element is neither a category or a script.
Parent: ${parent.category}, element: ${JSON.stringify(data)}`); Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
} }
} }
function isScript(data: any): boolean { function isScript(data: CategoryOrScriptData): data is ScriptData {
return (data.code && data.code.length > 0) const holder = (data as InstructionHolder);
|| data.call; return hasCode(holder) || hasCall(holder);
} }
function isCategory(data: any): boolean { function isCategory(data: CategoryOrScriptData): data is CategoryData {
return data.category && data.category.length > 0; 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;
} }

View File

@@ -1,61 +1,64 @@
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*'; import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> { export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
if (!documentable) { if (!documentable) {
throw new Error('documentable is null or undefined'); throw new Error('documentable is null or undefined');
} }
const docs = documentable.docs; const { docs } = documentable;
if (!docs || !docs.length) { if (!docs || !docs.length) {
return []; return [];
} }
let result = new DocumentationUrlContainer(); let result = new DocumentationUrlContainer();
result = addDocs(docs, result); result = addDocs(docs, result);
return result.getAll(); return result.getAll();
} }
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer { function addDocs(
if (docs instanceof Array) { docs: DocumentationUrlsData,
urls.addUrls(docs); urls: DocumentationUrlContainer,
} else if (typeof docs === 'string') { ): DocumentationUrlContainer {
urls.addUrl(docs); if (docs instanceof Array) {
} else { urls.addUrls(docs);
throw new Error('Docs field (documentation url) must a string or array of strings'); } else if (typeof docs === 'string') {
} urls.addUrl(docs);
return urls; } else {
throw new Error('Docs field (documentation url) must a string or array of strings');
}
return urls;
} }
class DocumentationUrlContainer { class DocumentationUrlContainer {
private readonly urls = new Array<string>(); private readonly urls = new Array<string>();
public addUrl(url: string) { public addUrl(url: string) {
validateUrl(url); validateUrl(url);
this.urls.push(url); this.urls.push(url);
} }
public addUrls(urls: readonly any[]) { public addUrls(urls: readonly string[]) {
for (const url of urls) { for (const url of urls) {
if (typeof url !== 'string') { if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings'); throw new Error('Docs field (documentation url) must be an array of strings');
} }
this.addUrl(url); this.addUrl(url);
}
} }
}
public getAll(): ReadonlyArray<string> { public getAll(): ReadonlyArray<string> {
return this.urls; return this.urls;
} }
} }
function validateUrl(docUrl: string): void { function validateUrl(docUrl: string): void {
if (!docUrl) { if (!docUrl) {
throw new Error('Documentation url is null or empty'); throw new Error('Documentation url is null or empty');
} }
if (docUrl.includes('\n')) { if (docUrl.includes('\n')) {
throw new Error('Documentation url cannot be multi-lined.'); throw new Error('Documentation url cannot be multi-lined.');
} }
const res = docUrl.match( const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
/(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) { if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`); throw new Error(`Invalid documentation url: ${docUrl}`);
} }
} }

View File

@@ -2,11 +2,12 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation'; import { ProjectInformation } from '@/domain/ProjectInformation';
export function parseProjectInformation( export function parseProjectInformation(
environment: NodeJS.ProcessEnv): IProjectInformation { environment: NodeJS.ProcessEnv,
return new ProjectInformation( ): IProjectInformation {
environment.VUE_APP_NAME, return new ProjectInformation(
environment.VUE_APP_VERSION, environment.VUE_APP_NAME,
environment.VUE_APP_REPOSITORY_URL, environment.VUE_APP_VERSION,
environment.VUE_APP_HOMEPAGE_URL, environment.VUE_APP_REPOSITORY_URL,
); environment.VUE_APP_HOMEPAGE_URL,
);
} }

View File

@@ -1,6 +1,6 @@
import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptCompiler } from './Compiler/IScriptCompiler'; import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { ScriptCompiler } from './Compiler/ScriptCompiler'; import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
@@ -8,15 +8,17 @@ import { SyntaxFactory } from './Syntax/SyntaxFactory';
import { ISyntaxFactory } from './Syntax/ISyntaxFactory'; import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext { export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
public readonly compiler: IScriptCompiler; public readonly compiler: IScriptCompiler;
public readonly syntax: ILanguageSyntax;
constructor( public readonly syntax: ILanguageSyntax;
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition, constructor(
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) { functionsData: ReadonlyArray<FunctionData> | undefined,
if (!scripting) { throw new Error('undefined scripting'); } scripting: IScriptingDefinition,
this.syntax = syntaxFactory.create(scripting.language); syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
this.compiler = new ScriptCompiler(functionsData, this.syntax); ) {
} if (!scripting) { throw new Error('undefined scripting'); }
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax);
}
} }

View File

@@ -1,62 +1,63 @@
import { ExpressionPosition } from './ExpressionPosition'; import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { IExpression } from './IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { IExpression } from './IExpression';
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string; export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression { export class Expression implements IExpression {
constructor( constructor(
public readonly position: ExpressionPosition, public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator,
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) { public readonly parameters
if (!position) { : IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
throw new Error('undefined position'); ) {
} if (!position) {
if (!evaluator) { throw new Error('undefined position');
throw new Error('undefined evaluator');
}
} }
public evaluate(context: IExpressionEvaluationContext): string { if (!evaluator) {
if (!context) { throw new Error('undefined evaluator');
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);
} }
}
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( function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
) { ) {
const requiredParameterNames = parameters const requiredParameterNames = parameters
.all .all
.filter((parameter) => !parameter.isOptional) .filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name); .map((parameter) => parameter.name);
const missingParameterNames = requiredParameterNames const missingParameterNames = requiredParameterNames
.filter((parameterName) => !args.hasArgument(parameterName)); .filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) { if (missingParameterNames.length) {
throw new Error( throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`); `argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
} );
}
} }
function filterUnusedArguments( function filterUnusedArguments(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection { allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
const specificCallArgs = new FunctionCallArgumentCollection(); ): IReadOnlyFunctionCallArgumentCollection {
for (const parameter of parameters.all) { const specificCallArgs = new FunctionCallArgumentCollection();
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) { parameters.all
continue; // Optional parameter is not necessarily provided .filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
} .map((parameter) => allFunctionArgs.getArgument(parameter.name))
const arg = allFunctionArgs.getArgument(parameter.name); .forEach((argument) => specificCallArgs.addArgument(argument));
specificCallArgs.addArgument(arg); return specificCallArgs;
}
return specificCallArgs;
} }

View File

@@ -3,16 +3,17 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
import { PipelineCompiler } from '../Pipes/PipelineCompiler'; import { PipelineCompiler } from '../Pipes/PipelineCompiler';
export interface IExpressionEvaluationContext { export interface IExpressionEvaluationContext {
readonly args: IReadOnlyFunctionCallArgumentCollection; readonly args: IReadOnlyFunctionCallArgumentCollection;
readonly pipelineCompiler: IPipelineCompiler; readonly pipelineCompiler: IPipelineCompiler;
} }
export class ExpressionEvaluationContext implements IExpressionEvaluationContext { export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
constructor( constructor(
public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) { public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
if (!args) { ) {
throw new Error('undefined args, send empty collection instead'); if (!args) {
} throw new Error('undefined args, send empty collection instead');
} }
}
} }

View File

@@ -1,15 +1,16 @@
export class ExpressionPosition { export class ExpressionPosition {
constructor( constructor(
public readonly start: number, public readonly start: number,
public readonly end: number) { public readonly end: number,
if (start === end) { ) {
throw new Error(`no length (start = end = ${start})`); 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}`);
}
} }
if (start > end) {
throw Error(`start (${start}) after end (${end})`);
}
if (start < 0) {
throw Error(`negative start position: ${start}`);
}
}
} }

View File

@@ -1,9 +1,9 @@
import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { ExpressionPosition } from './ExpressionPosition';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export interface IExpression { export interface IExpression {
readonly position: ExpressionPosition; readonly position: ExpressionPosition;
readonly parameters: IReadOnlyFunctionParameterCollection; readonly parameters: IReadOnlyFunctionParameterCollection;
evaluate(context: IExpressionEvaluationContext): string; evaluate(context: IExpressionEvaluationContext): string;
} }

View File

@@ -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 { IExpressionsCompiler } from './IExpressionsCompiler';
import { IExpression } from './Expression/IExpression'; import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser'; import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; 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 { export class ExpressionsCompiler implements IExpressionsCompiler {
public constructor( public constructor(
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { } private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
public compileExpressions( ) { }
code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string { public compileExpressions(
if (!args) { code: string | undefined,
throw new Error('undefined args, send empty collection instead'); args: IReadOnlyFunctionCallArgumentCollection,
} ): string {
if (!code) { if (!args) {
return code; throw new Error('undefined args, send empty collection instead');
}
const expressions = this.extractor.findExpressions(code);
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileExpressions(expressions, code, context);
return compiledCode;
} }
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( function compileExpressions(
expressions: readonly IExpression[], expressions: readonly IExpression[],
code: string, code: string,
context: IExpressionEvaluationContext) { context: IExpressionEvaluationContext,
let compiledCode = ''; ) {
const sortedExpressions = expressions let compiledCode = '';
.slice() // copy the array to not mutate the parameter const sortedExpressions = expressions
.sort((a, b) => b.position.start - a.position.start); .slice() // copy the array to not mutate the parameter
let index = 0; .sort((a, b) => b.position.start - a.position.start);
while (index !== code.length) { let index = 0;
const nextExpression = sortedExpressions.pop(); while (index !== code.length) {
if (nextExpression) { const nextExpression = sortedExpressions.pop();
compiledCode += code.substring(index, nextExpression.position.start); if (nextExpression) {
const expressionCode = nextExpression.evaluate(context); compiledCode += code.substring(index, nextExpression.position.start);
compiledCode += expressionCode; const expressionCode = nextExpression.evaluate(context);
index = nextExpression.position.end; compiledCode += expressionCode;
} else { index = nextExpression.position.end;
compiledCode += code.substring(index, code.length); } else {
break; compiledCode += code.substring(index, code.length);
} break;
} }
return compiledCode; }
return compiledCode;
} }
function extractRequiredParameterNames( function extractRequiredParameterNames(
expressions: readonly IExpression[]): string[] { expressions: readonly IExpression[],
const usedParameterNames = expressions ): string[] {
.map((e) => e.parameters.all const usedParameterNames = expressions
.filter((p) => !p.isOptional) .map((e) => e.parameters.all
.map((p) => p.name)) .filter((p) => !p.isOptional)
.filter((p) => p) .map((p) => p.name))
.flat(); .filter((p) => p)
const uniqueParameterNames = Array.from(new Set(usedParameterNames)); .flat();
return uniqueParameterNames; const uniqueParameterNames = Array.from(new Set(usedParameterNames));
return uniqueParameterNames;
} }
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[], expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void { providedArgs: IReadOnlyFunctionCallArgumentCollection,
const usedParameterNames = extractRequiredParameterNames(expressions); ): void {
if (!usedParameterNames?.length) { const usedParameterNames = extractRequiredParameterNames(expressions);
return; if (!usedParameterNames?.length) {
} return;
const notProvidedParameters = usedParameterNames }
.filter((parameterName) => !providedArgs.hasArgument(parameterName)); const notProvidedParameters = usedParameterNames
if (notProvidedParameters.length) { .filter((parameterName) => !providedArgs.hasArgument(parameterName));
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`); if (notProvidedParameters.length) {
} throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
}
} }
function printList(list: readonly string[]): string { function printList(list: readonly string[]): string {
return `"${list.join('", "')}"`; return `"${list.join('", "')}"`;
} }

View File

@@ -1,7 +1,7 @@
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
export interface IExpressionsCompiler { export interface IExpressionsCompiler {
compileExpressions( compileExpressions(
code: string | undefined, code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string; args: IReadOnlyFunctionCallArgumentCollection): string;
} }

View File

@@ -1,27 +1,28 @@
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../Expression/IExpression';
import { IExpressionParser } from './IExpressionParser';
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser'; import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
import { WithParser } from '../SyntaxParsers/WithParser'; import { WithParser } from '../SyntaxParsers/WithParser';
import { IExpressionParser } from './IExpressionParser';
const Parsers = [ const Parsers = [
new ParameterSubstitutionParser(), new ParameterSubstitutionParser(),
new WithParser(), new WithParser(),
]; ];
export class CompositeExpressionParser implements IExpressionParser { export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (leafs.some((leaf) => !leaf)) { if (leafs.some((leaf) => !leaf)) {
throw new Error('undefined leaf'); throw new Error('undefined leaf');
}
} }
public findExpressions(code: string): IExpression[] { }
const expressions = new Array<IExpression>();
for (const parser of this.leafs) { public findExpressions(code: string): IExpression[] {
const newExpressions = parser.findExpressions(code); const expressions = new Array<IExpression>();
if (newExpressions && newExpressions.length) { for (const parser of this.leafs) {
expressions.push(...newExpressions); const newExpressions = parser.findExpressions(code);
} if (newExpressions && newExpressions.length) {
} expressions.push(...newExpressions);
return expressions; }
} }
return expressions;
}
} }

View File

@@ -1,5 +1,5 @@
import { IExpression } from '../Expression/IExpression'; import { IExpression } from '../Expression/IExpression';
export interface IExpressionParser { export interface IExpressionParser {
findExpressions(code: string): IExpression[]; findExpressions(code: string): IExpression[];
} }

View File

@@ -1,59 +1,60 @@
export class ExpressionRegexBuilder { export class ExpressionRegexBuilder {
private readonly parts = new Array<string>(); private readonly parts = new Array<string>();
public expectCharacters(characters: string) { public expectCharacters(characters: string) {
return this.addRawRegex( return this.addRawRegex(
characters characters
.replaceAll('$', '\\$') .replaceAll('$', '\\$')
.replaceAll('.', '\\.'), .replaceAll('.', '\\.'),
); );
} }
public expectOneOrMoreWhitespaces() { public expectOneOrMoreWhitespaces() {
return this return this
.addRawRegex('\\s+'); .addRawRegex('\\s+');
} }
public matchPipeline() { public matchPipeline() {
return this return this
.expectZeroOrMoreWhitespaces() .expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?'); .addRawRegex('(\\|\\s*.+?)?');
} }
public matchUntilFirstWhitespace() { public matchUntilFirstWhitespace() {
return this return this
.addRawRegex('([^|\\s]+)'); .addRawRegex('([^|\\s]+)');
} }
public matchAnythingExceptSurroundingWhitespaces() { public matchAnythingExceptSurroundingWhitespaces() {
return this return this
.expectZeroOrMoreWhitespaces() .expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)') .addRawRegex('(.+?)')
.expectZeroOrMoreWhitespaces(); .expectZeroOrMoreWhitespaces();
} }
public expectExpressionStart() { public expectExpressionStart() {
return this return this
.expectCharacters('{{') .expectCharacters('{{')
.expectZeroOrMoreWhitespaces(); .expectZeroOrMoreWhitespaces();
} }
public expectExpressionEnd() { public expectExpressionEnd() {
return this return this
.expectZeroOrMoreWhitespaces() .expectZeroOrMoreWhitespaces()
.expectCharacters('}}'); .expectCharacters('}}');
} }
public buildRegExp(): RegExp { public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g'); return new RegExp(this.parts.join(''), 'g');
} }
private expectZeroOrMoreWhitespaces() { private expectZeroOrMoreWhitespaces() {
return this return this
.addRawRegex('\\s*'); .addRawRegex('\\s*');
} }
private addRawRegex(regex: string) {
this.parts.push(regex); private addRawRegex(regex: string) {
return this; this.parts.push(regex);
} return this;
}
} }

View File

@@ -6,46 +6,47 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
export abstract class RegexParser implements IExpressionParser { export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp; protected abstract readonly regex: RegExp;
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code)); 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');
} }
const matches = Array.from(code.matchAll(this.regex));
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression; for (const match of matches) {
const startPos = match.index;
private* findRegexExpressions(code: string): Iterable<IExpression> { const endPos = startPos + match[0].length;
if (!code) { let position: ExpressionPosition;
throw new Error('undefined code'); try {
} position = new ExpressionPosition(startPos, endPos);
const matches = Array.from(code.matchAll(this.regex)); } catch (error) {
for (const match of matches) { throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
const startPos = match.index; }
const endPos = startPos + match[0].length; const primitiveExpression = this.buildExpression(match);
let position: ExpressionPosition; const parameters = getParameters(primitiveExpression);
try { const expression = new Expression(position, primitiveExpression.evaluator, parameters);
position = new ExpressionPosition(startPos, endPos); yield expression;
} 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 { export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator; evaluator: ExpressionEvaluator;
parameters?: readonly IFunctionParameter[]; parameters?: readonly IFunctionParameter[];
} }
function getParameters( function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection { expression: IPrimitiveExpression,
const parameters = new FunctionParameterCollection(); ): FunctionParameterCollection {
for (const parameter of expression.parameters || []) { const parameters = new FunctionParameterCollection();
parameters.addParameter(parameter); for (const parameter of expression.parameters || []) {
} parameters.addParameter(parameter);
return parameters; }
return parameters;
} }

View File

@@ -1,4 +1,4 @@
export interface IPipe { export interface IPipe {
readonly name: string; readonly name: string;
apply(input: string): string; apply(input: string): string;
} }

View File

@@ -1,3 +1,3 @@
export interface IPipelineCompiler { export interface IPipelineCompiler {
compile(value: string, pipeline: string): string; compile(value: string, pipeline: string): string;
} }

View File

@@ -1,27 +1,30 @@
import { IPipe } from '../IPipe'; import { IPipe } from '../IPipe';
export class EscapeDoubleQuotes implements IPipe { export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes'; public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
return raw?.replaceAll('"', '"^""'); public apply(raw: string): string {
/* return raw?.replaceAll('"', '"^""');
"^"" is the most robust and stable choice. /* eslint-disable max-len */
Other options: /*
"" "^"" is the most robust and stable choice.
Breaks, because it is fundamentally unsupported Other options:
"""" ""
Does not work with consecutive double quotes. Breaks, because it is fundamentally unsupported
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";" """"
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";" Does not work with consecutive double quotes.
\" E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
May break as they are interpreted by cmd.exe as metacharacters breaking the command Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
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"^""'" 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 "&"
Normalizes interior whitespace Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces \""
Works when using "^"": PowerShell -Command ""^""a& c"^"".length" Normalizes interior whitespace
A good explanation: https://stackoverflow.com/a/31413730 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 */
}
} }

View File

@@ -1,155 +1,166 @@
import { IPipe } from '../IPipe'; import { IPipe } from '../IPipe';
export class InlinePowerShell implements IPipe { export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell'; public readonly name: string = 'inlinePowerShell';
public apply(code: string): string {
if (!code || !hasLines(code)) { public apply(code: string): string {
return code; 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('; ');
} }
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) { 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.. #> Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code Otherwise single # comments out rest of the code
*/ */
function inlineComments(code: string): string { function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => { const makeInlineComment = (comment: string) => {
const value = comment?.trim(); const value = comment?.trim();
if (!value) { if (!value) {
return '<##>'; return '<##>';
} }
return `<# ${value} #>`; return `<# ${value} #>`;
}; };
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => { return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) { 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 match;
} }
return makeInlineComment(captureComment); return makeInlineComment(captureComment);
}); });
/* ------------------------------------
Other alternatives considered: /(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
-------------------------- ------------------------------------
/#(?<!<#)(?![<>])(.*)$/gm ✅ Covers all cases
------------------------- ❌ Matches every line, three capture groups are used to build result
✅ Simple, yet matches and captures only what's necessary ⏩ Usage
❌ Fails to match some cases return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>` (match, captureLeft, captureDash, captureComment) => {
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one` if (!captureDash) {
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one` return match;
❌ Uses lookbehind }
Safari does not yet support lookbehind and syntax, leading application to not return captureLeft + makeInlineComment(captureComment);
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);
});
*/ */
} }
function getLines(code: string): string [] { function getLines(code: string): string[] {
return (code?.split(/\r\n|\r|\n/) || []); return (code?.split(/\r\n|\r|\n/) || []);
} }
/* /*
Merges inline here-strings to a single lined string with Windows line terminator (\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 https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
*/ */
function mergeHereStrings(code: string) { function mergeHereStrings(code: string) {
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g; const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
return code.replaceAll(regex, (_$, quotes, scope) => { return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes); const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes); const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = getLines(escaped); const lines = getLines(escaped);
const inlined = lines.join(newString.separator); const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`; const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted; return quoted;
}); });
} }
interface IInlinedHereString { interface IInlinedHereString {
readonly quotesAround: string; readonly quotesAround: string;
readonly escapedQuotes: string; readonly escapedQuotes: string;
readonly separator: string; readonly separator: string;
} }
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString { function getHereStringHandler(quotes: string): IInlinedHereString {
const expandableNewLine = '`r`n'; /*
switch (quotes) { We handle @' and @" differently.
case '\'': Single quotes are interpreted literally and doubles are expandable.
return { */
quotesAround: '\'', const expandableNewLine = '`r`n';
escapedQuotes: '\'\'', switch (quotes) {
separator: `\'+"${expandableNewLine}"+\'`, case '\'':
}; return {
case '"': quotesAround: '\'',
return { escapedQuotes: '\'\'',
quotesAround: '"', separator: `'+"${expandableNewLine}"+'`,
escapedQuotes: '`"', };
separator: expandableNewLine, case '"':
}; return {
default: quotesAround: '"',
throw new Error(`expected quotes: ${quotes}`); escapedQuotes: '`"',
} separator: expandableNewLine,
};
default:
throw new Error(`expected quotes: ${quotes}`);
}
} }
/* /*
Input -> Input ->
Get-Service * ` Get-Service * `
Sort-Object StartType ` Sort-Object StartType `
Format-Table Name, ServiceType, Status -AutoSize Format-Table Name, ServiceType, Status -AutoSize
Output -> Output ->
Get-Service * | Sort-Object StartType | Format-Table -AutoSize Get-Service * | Sort-Object StartType | Format-Table -AutoSize
*/ */
function mergeLinesWithBacktick(code: string) { function mergeLinesWithBacktick(code: string) {
/* /*
The regex actually wraps any whitespace character after backtick and before newline The regex actually wraps any whitespace character after backtick and before newline
However, this is not always the case for PowerShell. However, this is not always the case for PowerShell.
I see two behaviors: I see two behaviors:
1. If inside string, it's accepted (inside " or ') 1. If inside string, it's accepted (inside " or ')
2. If part of a command, PowerShell throws "An empty pipe element is not allowed" 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 However we don't need to be so robust and handle this complexity (yet), so for easier regex
we wrap it anyway we wrap it anyway
*/ */
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' '); 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('; ');
} }

View File

@@ -3,45 +3,48 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes'; import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
const RegisteredPipes = [ const RegisteredPipes = [
new EscapeDoubleQuotes(), new EscapeDoubleQuotes(),
new InlinePowerShell(), new InlinePowerShell(),
]; ];
export interface IPipeFactory { export interface IPipeFactory {
get(pipeName: string): IPipe; get(pipeName: string): IPipe;
} }
export class PipeFactory implements IPipeFactory { export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>(); private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) { constructor(pipes: readonly IPipe[] = RegisteredPipes) {
throw new Error('undefined pipe in list'); if (pipes.some((pipe) => !pipe)) {
} throw new Error('undefined pipe in list');
for (const pipe of pipes) {
this.registerPipe(pipe);
}
} }
public get(pipeName: string): IPipe { for (const pipe of pipes) {
validatePipeName(pipeName); this.registerPipe(pipe);
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)) { public get(pipeName: string): IPipe {
throw new Error(`Pipe name must be unique: "${pipe.name}"`); validatePipeName(pipeName);
} if (!this.pipes.has(pipeName)) {
this.pipes.set(pipe.name, pipe); 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) { function validatePipeName(name: string) {
if (!name) { if (!name) {
throw new Error('empty pipe name'); throw new Error('empty pipe name');
} }
if (!/^[a-z][A-Za-z]*$/.test(name)) { if (!/^[a-z][A-Za-z]*$/.test(name)) {
throw new Error(`Pipe name should be camelCase: "${name}"`); throw new Error(`Pipe name should be camelCase: "${name}"`);
} }
} }

View File

@@ -2,30 +2,32 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
import { IPipelineCompiler } from './IPipelineCompiler'; import { IPipelineCompiler } from './IPipelineCompiler';
export class PipelineCompiler implements IPipelineCompiler { export class PipelineCompiler implements IPipelineCompiler {
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { } constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline); public compile(value: string, pipeline: string): string {
const pipeNames = extractPipeNames(pipeline); ensureValidArguments(value, pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName)); const pipeNames = extractPipeNames(pipeline);
for (const pipe of pipes) { const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
value = pipe.apply(value); let valueInCompilation = value;
} for (const pipe of pipes) {
return value; valueInCompilation = pipe.apply(valueInCompilation);
} }
return valueInCompilation;
}
} }
function extractPipeNames(pipeline: string): string[] { function extractPipeNames(pipeline: string): string[] {
return pipeline return pipeline
.trim() .trim()
.split('|') .split('|')
.slice(1) .slice(1)
.map((p) => p.trim()); .map((p) => p.trim());
} }
function ensureValidArguments(value: string, pipeline: string) { function ensureValidArguments(value: string, pipeline: string) {
if (!value) { throw new Error('undefined value'); } if (!value) { throw new Error('undefined value'); }
if (!pipeline) { throw new Error('undefined pipeline'); } if (!pipeline) { throw new Error('undefined pipeline'); }
if (!pipeline.trimStart().startsWith('|')) { if (!pipeline.trimStart().startsWith('|')) {
throw new Error('pipeline does not start with pipe'); throw new Error('pipeline does not start with pipe');
} }
} }

View File

@@ -1,28 +1,28 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser { export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder() protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('$') .expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name .matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline .matchPipeline() // Second match: Pipeline
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression { protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1]; const parameterName = match[1];
const pipeline = match[2]; const pipeline = match[2];
return { return {
parameters: [ new FunctionParameter(parameterName, false) ], parameters: [new FunctionParameter(parameterName, false)],
evaluator: (context) => { evaluator: (context) => {
const argumentValue = context.args.getArgument(parameterName).argumentValue; const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) { if (!pipeline) {
return argumentValue; return argumentValue;
} }
return context.pipelineCompiler.compile(argumentValue, pipeline); return context.pipelineCompiler.compile(argumentValue, pipeline);
}, },
}; };
} }
} }

View File

@@ -1,58 +1,59 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class WithParser extends RegexParser { export class WithParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder() protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }} // {{ 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 }}
.expectExpressionStart() .expectExpressionStart()
.expectCharacters('.') .expectCharacters('with')
.matchPipeline() // First match: pipeline .expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) { protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those const parameterName = match[1];
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => { const scopeText = match[2];
return replacer(match1); 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);
});
} }

View File

@@ -1,13 +1,14 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator'; import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
import { IFunctionCallArgument } from './IFunctionCallArgument';
export class FunctionCallArgument implements IFunctionCallArgument { export class FunctionCallArgument implements IFunctionCallArgument {
constructor( constructor(
public readonly parameterName: string, public readonly parameterName: string,
public readonly argumentValue: string) { public readonly argumentValue: string,
ensureValidParameterName(parameterName); ) {
if (!argumentValue) { ensureValidParameterName(parameterName);
throw new Error(`undefined argument value for "${parameterName}"`); if (!argumentValue) {
} throw new Error(`undefined argument value for "${parameterName}"`);
} }
}
} }

View File

@@ -2,33 +2,37 @@ import { IFunctionCallArgument } from './IFunctionCallArgument';
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection'; import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection { export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>(); private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) { public addArgument(argument: IFunctionCallArgument): void {
throw new Error('undefined argument'); 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);
} }
public getAllParameterNames(): string[] { if (this.hasArgument(argument.parameterName)) {
return Array.from(this.arguments.keys()); throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
} }
public hasArgument(parameterName: string): boolean { this.arguments.set(argument.parameterName, argument);
if (!parameterName) { }
throw new Error('undefined parameter name');
} public getAllParameterNames(): string[] {
return this.arguments.has(parameterName); return Array.from(this.arguments.keys());
}
public hasArgument(parameterName: string): boolean {
if (!parameterName) {
throw new Error('undefined parameter name');
} }
public getArgument(parameterName: string): IFunctionCallArgument { return this.arguments.has(parameterName);
if (!parameterName) { }
throw new Error('undefined parameter name');
} public getArgument(parameterName: string): IFunctionCallArgument {
const arg = this.arguments.get(parameterName); if (!parameterName) {
if (!arg) { throw new Error('undefined parameter name');
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
} }
const arg = this.arguments.get(parameterName);
if (!arg) {
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
}
} }

View File

@@ -1,4 +1,4 @@
export interface IFunctionCallArgument { export interface IFunctionCallArgument {
readonly parameterName: string; readonly parameterName: string;
readonly argumentValue: string; readonly argumentValue: string;
} }

View File

@@ -1,11 +1,11 @@
import { IFunctionCallArgument } from './IFunctionCallArgument'; import { IFunctionCallArgument } from './IFunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection { export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): IFunctionCallArgument; getArgument(parameterName: string): IFunctionCallArgument;
getAllParameterNames(): string[]; getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean; hasArgument(parameterName: string): boolean;
} }
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection { export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: IFunctionCallArgument): void; addArgument(argument: IFunctionCallArgument): void;
} }

View File

@@ -1,139 +1,149 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; 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 { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler'; import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction'; import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '../FunctionCall'; import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection'; 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 { export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler(); public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor( protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
} ) {
}
public compileCall( public compileCall(
calls: IFunctionCall[], calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode { functions: ISharedFunctionCollection,
if (!functions) { throw new Error('undefined functions'); } ): ICompiledCode {
if (!calls) { throw new Error('undefined calls'); } if (!functions) { throw new Error('undefined functions'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); } if (!calls) { throw new Error('undefined calls'); }
const context: ICompilationContext = { if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
allFunctions: functions, const context: ICompilationContext = {
callSequence: calls, allFunctions: functions,
expressionsCompiler: this.expressionsCompiler, callSequence: calls,
}; expressionsCompiler: this.expressionsCompiler,
const code = compileCallSequence(context); };
return code; const code = compileCallSequence(context);
} return code;
}
} }
interface ICompilationContext { interface ICompilationContext {
allFunctions: ISharedFunctionCollection; allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[]; callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler; expressionsCompiler: IExpressionsCompiler;
} }
interface ICompiledFunctionCall { interface ICompiledFunctionCall {
readonly code: string; readonly code: string;
readonly revertCode: string; readonly revertCode: string;
} }
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = new Array<ICompiledFunctionCall>(); const compiledFunctions = new Array<ICompiledFunctionCall>();
for (const call of context.callSequence) { for (const call of context.callSequence) {
const compiledCode = compileSingleCall(call, context); const compiledCode = compileSingleCall(call, context);
compiledFunctions.push(...compiledCode); compiledFunctions.push(...compiledCode);
} }
return { return {
code: merge(compiledFunctions.map((f) => f.code)), code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)), revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
}; };
} }
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] { function compileSingleCall(
const func = context.allFunctions.getFunctionByName(call.functionName); call: IFunctionCall,
ensureThatCallArgumentsExistInParameterDefinition(func, call.args); context: ICompilationContext,
if (func.body.code) { // Function with inline code ): ICompiledFunctionCall[] {
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler); const func = context.allFunctions.getFunctionByName(call.functionName);
return [ compiledCode ]; ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
} else { // Function with inner calls if (func.body.code) { // Function with inline code
return func.body.calls const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
.map((innerCall) => { return [compiledCode];
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler); }
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs); // Function with inner calls
return compileSingleCall(compiledCall, context); return func.body.calls
}) .map((innerCall) => {
.flat(); const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
} const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
} }
function compileCode( function compileCode(
code: IFunctionCode, code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunctionCall { compiler: IExpressionsCompiler,
return { ): ICompiledFunctionCall {
code: compiler.compileExpressions(code.do, args), return {
revertCode: compiler.compileExpressions(code.revert, args), code: compiler.compileExpressions(code.do, args),
}; revertCode: compiler.compileExpressions(code.revert, args),
};
} }
function compileArgs( function compileArgs(
argsToCompile: IReadOnlyFunctionCallArgumentCollection, argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler, compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection { ): IReadOnlyFunctionCallArgumentCollection {
const compiledArgs = new FunctionCallArgumentCollection(); const compiledArgs = new FunctionCallArgumentCollection();
for (const parameterName of argsToCompile.getAllParameterNames()) { for (const parameterName of argsToCompile.getAllParameterNames()) {
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue; const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args); const compiledValue = compiler.compileExpressions(argumentValue, args);
const newArgument = new FunctionCallArgument(parameterName, compiledValue); const newArgument = new FunctionCallArgument(parameterName, compiledValue);
compiledArgs.addArgument(newArgument); compiledArgs.addArgument(newArgument);
} }
return compiledArgs; return compiledArgs;
} }
function merge(codeParts: readonly string[]): string { function merge(codeParts: readonly string[]): string {
return codeParts return codeParts
.filter((part) => part?.length > 0) .filter((part) => part?.length > 0)
.join('\n'); .join('\n');
} }
function ensureThatCallArgumentsExistInParameterDefinition( function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction, func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void { args: IReadOnlyFunctionCallArgumentCollection,
const callArgumentNames = args.getAllParameterNames(); ): void {
const functionParameterNames = func.parameters.all.map((param) => param.name) || []; const callArgumentNames = args.getAllParameterNames();
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames); const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
} }
function findUnexpectedParameters( function findUnexpectedParameters(
callArgumentNames: string[], callArgumentNames: string[],
functionParameterNames: string[]): string[] { functionParameterNames: string[],
if (!callArgumentNames.length && !functionParameterNames.length) { ): string[] {
return []; if (!callArgumentNames.length && !functionParameterNames.length) {
} return [];
return callArgumentNames }
.filter((callParam) => !functionParameterNames.includes(callParam)); return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
} }
function throwIfNotEmpty( function throwIfNotEmpty(
functionName: string, functionName: string,
unexpectedParameters: string[], unexpectedParameters: string[],
expectedParameters: string[]) { expectedParameters: string[],
if (!unexpectedParameters.length) { ) {
return; if (!unexpectedParameters.length) {
} return;
throw new Error( }
`Function "${functionName}" has unexpected parameter(s) provided: ` + throw new Error(
`"${unexpectedParameters.join('", "')}"` + // eslint-disable-next-line prefer-template
'. Expected parameter(s): ' + `Function "${functionName}" has unexpected parameter(s) provided: `
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + `"${unexpectedParameters.join('", "')}"`
); + '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
} }

View File

@@ -1,4 +1,4 @@
export interface ICompiledCode { export interface ICompiledCode {
readonly code: string; readonly code: string;
readonly revertCode?: string; readonly revertCode?: string;
} }

View File

@@ -1,9 +1,9 @@
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '../IFunctionCall'; import { IFunctionCall } from '../IFunctionCall';
import { ICompiledCode } from './ICompiledCode';
export interface IFunctionCallCompiler { export interface IFunctionCallCompiler {
compileCall( compileCall(
calls: IFunctionCall[], calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode; functions: ISharedFunctionCollection): ICompiledCode;
} }

View File

@@ -2,14 +2,15 @@ import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCal
import { IFunctionCall } from './IFunctionCall'; import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall { export class FunctionCall implements IFunctionCall {
constructor( constructor(
public readonly functionName: string, public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection) { public readonly args: IReadOnlyFunctionCallArgumentCollection,
if (!functionName) { ) {
throw new Error('empty function name in function call'); if (!functionName) {
} throw new Error('empty function name in function call');
if (!args) {
throw new Error('undefined args');
}
} }
if (!args) {
throw new Error('undefined args');
}
}
} }

View File

@@ -5,31 +5,31 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { FunctionCall } from './FunctionCall'; import { FunctionCall } from './FunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] { export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
if (!calls) { if (calls === undefined) {
throw new Error('undefined call data'); throw new Error('undefined call data');
} }
const sequence = getCallSequence(calls); const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call)); return sequence.map((call) => parseFunctionCall(call));
} }
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (typeof calls !== 'object') { if (typeof calls !== 'object') {
throw new Error('called function(s) must be an object'); throw new Error('called function(s) must be an object');
} }
if (calls instanceof Array) { if (calls instanceof Array) {
return calls as FunctionCallData[]; return calls as FunctionCallData[];
} }
return [ calls as FunctionCallData ]; return [calls as FunctionCallData];
} }
function parseFunctionCall(call: FunctionCallData): IFunctionCall { function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) { if (!call) {
throw new Error(`undefined function call`); throw new Error('undefined function call');
} }
const args = new FunctionCallArgumentCollection(); const args = new FunctionCallArgumentCollection();
for (const parameterName of Object.keys(call.parameters || {})) { for (const parameterName of Object.keys(call.parameters || {})) {
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]); const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
args.addArgument(arg); args.addArgument(arg);
} }
return new FunctionCall(call.function, args); return new FunctionCall(call.function, args);
} }

View File

@@ -1,6 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall { export interface IFunctionCall {
readonly functionName: string; readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection; readonly args: IReadOnlyFunctionCallArgumentCollection;
} }

View File

@@ -1,24 +1,24 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from '../Function/Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
export interface ISharedFunction { export interface ISharedFunction {
readonly name: string; readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection; readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody; readonly body: ISharedFunctionBody;
} }
export interface ISharedFunctionBody { export interface ISharedFunctionBody {
readonly type: FunctionBodyType; readonly type: FunctionBodyType;
readonly code: IFunctionCode; readonly code: IFunctionCode;
readonly calls: readonly IFunctionCall[]; readonly calls: readonly IFunctionCall[];
} }
export enum FunctionBodyType { export enum FunctionBodyType {
Code, Code,
Calls, Calls,
} }
export interface IFunctionCode { export interface IFunctionCode {
readonly do: string; readonly do: string;
readonly revert?: string; readonly revert?: string;
} }

View File

@@ -1,5 +1,5 @@
import { ISharedFunction } from './ISharedFunction'; import { ISharedFunction } from './ISharedFunction';
export interface ISharedFunctionCollection { export interface ISharedFunctionCollection {
getFunctionByName(name: string): ISharedFunction; getFunctionByName(name: string): ISharedFunction;
} }

View File

@@ -2,5 +2,5 @@ import { FunctionData } from 'js-yaml-loader!@/*';
import { ISharedFunctionCollection } from './ISharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface ISharedFunctionsParser { export interface ISharedFunctionsParser {
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection; parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
} }

View File

@@ -1,10 +1,11 @@
import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../Shared/ParameterNameValidator'; import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
import { IFunctionParameter } from './IFunctionParameter';
export class FunctionParameter implements IFunctionParameter { export class FunctionParameter implements IFunctionParameter {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly isOptional: boolean) { public readonly isOptional: boolean,
ensureValidParameterName(name); ) {
} ensureValidParameterName(name);
}
} }

View File

@@ -2,25 +2,27 @@ import { IFunctionParameterCollection } from './IFunctionParameterCollection';
import { IFunctionParameter } from './IFunctionParameter'; import { IFunctionParameter } from './IFunctionParameter';
export class FunctionParameterCollection implements IFunctionParameterCollection { export class FunctionParameterCollection implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>(); private parameters = new Array<IFunctionParameter>();
public get all(): readonly IFunctionParameter[] { public get all(): readonly IFunctionParameter[] {
return this.parameters; return this.parameters;
} }
public addParameter(parameter: IFunctionParameter) {
this.ensureValidParameter(parameter);
this.parameters.push(parameter);
}
private includesName(name: string) { public addParameter(parameter: IFunctionParameter) {
return this.parameters.find((existingParameter) => existingParameter.name === name); 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 (this.includesName(parameter.name)) {
if (!parameter) { throw new Error(`duplicate parameter name: "${parameter.name}"`);
throw new Error('undefined parameter');
}
if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`);
}
} }
}
} }

View File

@@ -1,4 +1,4 @@
export interface IFunctionParameter { export interface IFunctionParameter {
readonly name: string; readonly name: string;
readonly isOptional: boolean; readonly isOptional: boolean;
} }

View File

@@ -1,9 +1,9 @@
import { IFunctionParameter } from './IFunctionParameter'; import { IFunctionParameter } from './IFunctionParameter';
export interface IReadOnlyFunctionParameterCollection { export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly IFunctionParameter[]; readonly all: readonly IFunctionParameter[];
} }
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection { export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: IFunctionParameter): void; addParameter(parameter: IFunctionParameter): void;
} }

View File

@@ -1,8 +1,8 @@
export function ensureValidParameterName(parameterName: string) { export function ensureValidParameterName(parameterName: string) {
if (!parameterName) { if (!parameterName) {
throw new Error('undefined parameter name'); throw new Error('undefined parameter name');
} }
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) { if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`); throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
} }
} }

View File

@@ -1,49 +1,54 @@
import { IFunctionCall } from '../Function/Call/IFunctionCall'; import { IFunctionCall } from './Call/IFunctionCall';
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction'; import {
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
} from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export function createCallerFunction( export function createCallerFunction(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
callSequence: readonly IFunctionCall[]): ISharedFunction { callSequence: readonly IFunctionCall[],
if (!callSequence) { ): ISharedFunction {
throw new Error(`undefined call sequence in function "${name}"`); if (!callSequence) {
} throw new Error(`undefined call sequence in function "${name}"`);
if (!callSequence.length) { }
throw new Error(`empty 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); }
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
} }
export function createFunctionWithInlineCode( export function createFunctionWithInlineCode(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
code: string, code: string,
revertCode?: string): ISharedFunction { revertCode?: string,
if (!code) { ): ISharedFunction {
throw new Error(`undefined code in function "${name}"`); if (!code) {
} throw new Error(`undefined code in function "${name}"`);
const content: IFunctionCode = { }
do: code, const content: IFunctionCode = {
revert: revertCode, do: code,
}; revert: revertCode,
return new SharedFunction(name, parameters, content, FunctionBodyType.Code); };
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
} }
class SharedFunction implements ISharedFunction { class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody; public readonly body: ISharedFunctionBody;
constructor(
public readonly name: string, constructor(
public readonly parameters: IReadOnlyFunctionParameterCollection, public readonly name: string,
content: IFunctionCode | readonly IFunctionCall[], public readonly parameters: IReadOnlyFunctionParameterCollection,
bodyType: FunctionBodyType, content: IFunctionCode | readonly IFunctionCall[],
) { bodyType: FunctionBodyType,
if (!name) { throw new Error('undefined function name'); } ) {
if (!parameters) { throw new Error(`undefined parameters`); } if (!name) { throw new Error('undefined function name'); }
this.body = { if (!parameters) { throw new Error('undefined parameters'); }
type: bodyType, this.body = {
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, type: bodyType,
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined, code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
}; calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
} };
}
} }

View File

@@ -2,26 +2,26 @@ import { ISharedFunction } from './ISharedFunction';
import { ISharedFunctionCollection } from './ISharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export class SharedFunctionCollection implements 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 { public addFunction(func: ISharedFunction): void {
if (!func) { throw new Error('undefined function'); } if (!func) { throw new Error('undefined function'); }
if (this.has(func.name)) { if (this.has(func.name)) {
throw new Error(`function with name ${func.name} already exists`); throw new Error(`function with name ${func.name} already exists`);
}
this.functionsByName.set(func.name, func);
} }
this.functionsByName.set(func.name, func);
}
public getFunctionByName(name: string): ISharedFunction { public getFunctionByName(name: string): ISharedFunction {
if (!name) { throw Error('undefined function name'); } if (!name) { throw Error('undefined function name'); }
const func = this.functionsByName.get(name); const func = this.functionsByName.get(name);
if (!func) { if (!func) {
throw new Error(`called function is not defined "${name}"`); throw new Error(`called function is not defined "${name}"`);
}
return func;
} }
return func;
}
private has(functionName: string) { private has(functionName: string) {
return this.functionsByName.has(functionName); return this.functionsByName.has(functionName);
} }
} }

View File

@@ -10,131 +10,132 @@ import { ISharedFunction } from './ISharedFunction';
import { parseFunctionCalls } from './Call/FunctionCallParser'; import { parseFunctionCalls } from './Call/FunctionCallParser';
export class SharedFunctionsParser implements ISharedFunctionsParser { export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser(); public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
public parseFunctions(
functions: readonly FunctionData[]): ISharedFunctionCollection { public parseFunctions(
const collection = new SharedFunctionCollection(); functions: readonly FunctionData[],
if (!functions || !functions.length) { ): ISharedFunctionCollection {
return collection; const collection = new SharedFunctionCollection();
} if (!functions || !functions.length) {
ensureValidFunctions(functions); return collection;
for (const func of functions) {
const sharedFunction = parseFunction(func);
collection.addFunction(sharedFunction);
}
return collection;
} }
ensureValidFunctions(functions);
for (const func of functions) {
const sharedFunction = parseFunction(func);
collection.addFunction(sharedFunction);
}
return collection;
}
} }
function parseFunction(data: FunctionData): ISharedFunction { function parseFunction(data: FunctionData): ISharedFunction {
const name = data.name; const { name } = data;
const parameters = parseParameters(data); const parameters = parseParameters(data);
if (hasCode(data)) { if (hasCode(data)) {
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} else { // has call }
const calls = parseFunctionCalls(data.call); // Has call
return createCallerFunction(name, parameters, calls); const calls = parseFunctionCalls(data.call);
} return createCallerFunction(name, parameters, calls);
} }
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection(); const parameters = new FunctionParameterCollection();
if (!data.parameters) { 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; 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 { function hasCode(data: FunctionData): boolean {
return Boolean(data.code); return Boolean(data.code);
} }
function hasCall(data: FunctionData): boolean { function hasCall(data: FunctionData): boolean {
return Boolean(data.call); return Boolean(data.call);
} }
function ensureValidFunctions(functions: readonly FunctionData[]) { function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions); ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions); ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicateCode(functions); ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions); ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParametersType(functions); ensureExpectedParametersType(functions);
} }
function printList(list: readonly string[]): string { function printList(list: readonly string[]): string {
return `"${list.join('","')}"`; return `"${list.join('","')}"`;
} }
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) { function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
// Ensure functions do not define both call and code // Ensure functions do not define both call and code
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder)); const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
if (withBothCallAndCode.length) { if (withBothCallAndCode.length) {
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`); throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
} }
// Ensure functions have either code or call // Ensure functions have either code or call
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder)); const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
if (hasEitherCodeOrCall.length) { if (hasEitherCodeOrCall.length) {
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`); throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
} }
} }
function ensureExpectedParametersType(functions: readonly FunctionData[]) { function ensureExpectedParametersType(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions const unexpectedFunctions = functions
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters)); .filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
if (unexpectedFunctions.length) { if (unexpectedFunctions.length) {
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`; const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
throw new Error(errorMessage); throw new Error(errorMessage);
} }
} }
function isArrayOfObjects(value: any): boolean { function isArrayOfObjects(value: unknown): boolean {
return Array.isArray(value) return Array.isArray(value)
&& value.every((item) => typeof item === 'object'); && value.every((item) => typeof item === 'object');
} }
function printNames(holders: readonly InstructionHolder[]) { function printNames(holders: readonly InstructionHolder[]) {
return printList(holders.map((holder) => holder.name)); return printList(holders.map((holder) => holder.name));
} }
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) { function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
const duplicateFunctionNames = getDuplicates(functions const duplicateFunctionNames = getDuplicates(functions
.map((func) => func.name.toLowerCase())); .map((func) => func.name.toLowerCase()));
if (duplicateFunctionNames.length) { if (duplicateFunctionNames.length) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`); throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
} }
} }
function ensureNoUndefinedItem(functions: readonly FunctionData[]) { function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) { if (functions.some((func) => !func)) {
throw new Error(`some functions are undefined`); throw new Error('some functions are undefined');
} }
} }
function ensureNoDuplicateCode(functions: readonly FunctionData[]) { function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions const duplicateCodes = getDuplicates(functions
.map((func) => func.code) .map((func) => func.code)
.filter((code) => code), .filter((code) => code));
); if (duplicateCodes.length > 0) {
if (duplicateCodes.length > 0) { throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); }
} const duplicateRevertCodes = getDuplicates(functions
const duplicateRevertCodes = getDuplicates(functions .filter((func) => func.revertCode)
.filter((func) => func.revertCode) .map((func) => func.revertCode));
.map((func) => func.revertCode)); if (duplicateRevertCodes.length > 0) {
if (duplicateRevertCodes.length > 0) { throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); }
}
} }
function getDuplicates(texts: readonly string[]): string[] { function getDuplicates(texts: readonly string[]): string[] {
return texts.filter((item, index) => texts.indexOf(item) !== index); return texts.filter((item, index) => texts.indexOf(item) !== index);
} }

View File

@@ -1,7 +1,7 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptData } from 'js-yaml-loader!@/*'; import { ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode';
export interface IScriptCompiler { export interface IScriptCompiler {
canCompile(script: ScriptData): boolean; canCompile(script: ScriptData): boolean;
compile(script: ScriptData): IScriptCode; compile(script: ScriptData): IScriptCode;
} }

View File

@@ -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 { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCompiler } from './IScriptCompiler'; import { IScriptCompiler } from './IScriptCompiler';
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler'; import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
@@ -11,34 +10,38 @@ import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
export class ScriptCompiler implements IScriptCompiler { export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection; private readonly functions: ISharedFunctionCollection;
constructor(
functions: readonly FunctionData[] | undefined, constructor(
private readonly syntax: ILanguageSyntax, functions: readonly FunctionData[] | undefined,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, private readonly syntax: ILanguageSyntax,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
) { sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
if (!syntax) { throw new Error('undefined syntax'); } ) {
this.functions = sharedFunctionsParser.parseFunctions(functions); 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 { return true;
if (!script) { throw new Error('undefined script'); } }
if (!script.call) {
return false; public compile(script: ScriptData): IScriptCode {
} if (!script) { throw new Error('undefined script'); }
return true; try {
} const calls = parseFunctionCalls(script.call);
public compile(script: ScriptData): IScriptCode { const compiledCode = this.callCompiler.compileCall(calls, this.functions);
if (!script) { throw new Error('undefined script'); } return new ScriptCode(
try { compiledCode.code,
const calls = parseFunctionCalls(script.call); compiledCode.revertCode,
const compiledCode = this.callCompiler.compileCall(calls, this.functions); this.syntax,
return new ScriptCode( );
compiledCode.code, } catch (error) {
compiledCode.revertCode, throw Error(`Script "${script.name}" ${error.message}`);
this.syntax);
} catch (error) {
throw Error(`Script "${script.name}" ${error.message}`);
}
} }
}
} }

View File

@@ -2,6 +2,6 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptCompiler } from './Compiler/IScriptCompiler'; import { IScriptCompiler } from './Compiler/IScriptCompiler';
export interface ICategoryCollectionParseContext { export interface ICategoryCollectionParseContext {
readonly compiler: IScriptCompiler; readonly compiler: IScriptCompiler;
readonly syntax: ILanguageSyntax; readonly syntax: ILanguageSyntax;
} }

View File

@@ -1,54 +1,60 @@
import { Script } from '@/domain/Script';
import { ScriptData } from 'js-yaml-loader!@/*'; import { ScriptData } from 'js-yaml-loader!@/*';
import { parseDocUrls } from '../DocumentationParser'; import { Script } from '@/domain/Script';
import { RecommendationLevel } from '@/domain/RecommendationLevel'; import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { IScriptCode } from '@/domain/IScriptCode'; import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode'; import { ScriptCode } from '@/domain/ScriptCode';
import { parseDocUrls } from '../DocumentationParser';
import { createEnumParser, IEnumParser } from '../../Common/Enum'; import { createEnumParser, IEnumParser } from '../../Common/Enum';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext'; import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
export function parseScript( export function parseScript(
data: ScriptData, context: ICategoryCollectionParseContext, data: ScriptData,
levelParser = createEnumParser(RecommendationLevel)): Script { context: ICategoryCollectionParseContext,
validateScript(data); levelParser = createEnumParser(RecommendationLevel),
if (!context) { throw new Error('undefined context'); } ): Script {
const script = new Script( validateScript(data);
/* name */ data.name, if (!context) { throw new Error('undefined context'); }
/* code */ parseCode(data, context), const script = new Script(
/* docs */ parseDocUrls(data), /* name: */ data.name,
/* level */ parseLevel(data.recommend, levelParser)); /* code: */ parseCode(data, context),
return script; /* docs: */ parseDocUrls(data),
/* level: */ parseLevel(data.recommend, levelParser),
);
return script;
} }
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined { function parseLevel(
if (!level) { level: string,
return undefined; parser: IEnumParser<RecommendationLevel>,
} ): RecommendationLevel | undefined {
return parser.parseEnum(level, 'level'); if (!level) {
return undefined;
}
return parser.parseEnum(level, 'level');
} }
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode { function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
if (context.compiler.canCompile(script)) { if (context.compiler.canCompile(script)) {
return context.compiler.compile(script); return context.compiler.compile(script);
} }
return new ScriptCode(script.code, script.revertCode, context.syntax); return new ScriptCode(script.code, script.revertCode, context.syntax);
} }
function ensureNotBothCallAndCode(script: ScriptData) { function ensureNotBothCallAndCode(script: ScriptData) {
if (script.code && script.call) { if (script.code && script.call) {
throw new Error('cannot define both "call" and "code"'); throw new Error('cannot define both "call" and "code"');
} }
if (script.revertCode && script.call) { if (script.revertCode && script.call) {
throw new Error('cannot define "revertCode" if "call" is defined'); throw new Error('cannot define "revertCode" if "call" is defined');
} }
} }
function validateScript(script: ScriptData) { function validateScript(script: ScriptData) {
if (!script) { if (!script) {
throw new Error('undefined script'); throw new Error('undefined script');
} }
if (!script.code && !script.call) { if (!script.code && !script.call) {
throw new Error('must define either "call" or "code"'); throw new Error('must define either "call" or "code"');
} }
ensureNotBothCallAndCode(script); ensureNotBothCallAndCode(script);
} }

View File

@@ -1,10 +1,10 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ]; const PowerShellCommonCodeParts = ['{', '}'];
const PowerShellCommonCodeParts = [ '{', '}' ];
export class BatchFileSyntax implements ILanguageSyntax { export class BatchFileSyntax implements ILanguageSyntax {
public readonly commentDelimiters = [ 'REM', '::' ]; public readonly commentDelimiters = ['REM', '::'];
public readonly commonCodeParts = [ ...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts ];
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
} }

View File

@@ -1,5 +1,4 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> { export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
}

View File

@@ -1,6 +1,7 @@
import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/domain/ScriptCode';
export class ShellScriptSyntax implements ILanguageSyntax { export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = [ '#' ]; public readonly commentDelimiters = ['#'];
public readonly commonCodeParts = [ '(', ')', 'else', 'fi' ];
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
} }

View File

@@ -5,10 +5,12 @@ import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory'; import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory { export class SyntaxFactory
constructor() { extends ScriptingLanguageFactory<ILanguageSyntax>
super(); implements ISyntaxFactory {
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax()); constructor() {
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax()); super();
} this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
}
} }

View File

@@ -3,34 +3,35 @@ import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compile
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICodeSubstituter } from './ICodeSubstituter';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ICodeSubstituter } from './ICodeSubstituter';
export class CodeSubstituter implements ICodeSubstituter { export class CodeSubstituter implements ICodeSubstituter {
constructor( constructor(
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(), private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
private readonly date = new Date(), private readonly date = new Date(),
) { ) {
} }
public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('undefined code'); } public substitute(code: string, info: IProjectInformation): string {
if (!info) { throw new Error('undefined info'); } if (!code) { throw new Error('undefined code'); }
const args = new FunctionCallArgumentCollection(); if (!info) { throw new Error('undefined info'); }
const substitute = (name: string, value: string) => const args = new FunctionCallArgumentCollection();
args.addArgument(new FunctionCallArgument(name, value)); const substitute = (name: string, value: string) => args
substitute('homepage', info.homepage); .addArgument(new FunctionCallArgument(name, value));
substitute('version', info.version); substitute('homepage', info.homepage);
substitute('date', this.date.toUTCString()); substitute('version', info.version);
const compiledCode = this.compiler.compileExpressions(code, args); substitute('date', this.date.toUTCString());
return compiledCode; const compiledCode = this.compiler.compileExpressions(code, args);
} return compiledCode;
}
} }
function createSubstituteCompiler(): IExpressionsCompiler { function createSubstituteCompiler(): IExpressionsCompiler {
const parsers = [ new ParameterSubstitutionParser() ]; const parsers = [new ParameterSubstitutionParser()];
const parser = new CompositeExpressionParser(parsers); const parser = new CompositeExpressionParser(parsers);
const expressionCompiler = new ExpressionsCompiler(parser); const expressionCompiler = new ExpressionsCompiler(parser);
return expressionCompiler; return expressionCompiler;
} }

View File

@@ -1,5 +1,5 @@
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
export interface ICodeSubstituter { export interface ICodeSubstituter {
substitute(code: string, info: IProjectInformation): string; substitute(code: string, info: IProjectInformation): string;
} }

View File

@@ -1,5 +1,5 @@
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*'; import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition'; import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IProjectInformation } from '@/domain/IProjectInformation'; import { IProjectInformation } from '@/domain/IProjectInformation';
@@ -8,24 +8,25 @@ import { ICodeSubstituter } from './ICodeSubstituter';
import { CodeSubstituter } from './CodeSubstituter'; import { CodeSubstituter } from './CodeSubstituter';
export class ScriptingDefinitionParser { export class ScriptingDefinitionParser {
constructor( constructor(
private readonly languageParser = createEnumParser(ScriptingLanguage), private readonly languageParser = createEnumParser(ScriptingLanguage),
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(), 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,
);
}
}
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,
);
}
}

View File

@@ -1,64 +1,64 @@
declare module 'js-yaml-loader!@/*' { declare module 'js-yaml-loader!@/*' {
export interface CollectionData { export interface CollectionData {
readonly os: string; readonly os: string;
readonly scripting: ScriptingDefinitionData; readonly scripting: ScriptingDefinitionData;
readonly actions: ReadonlyArray<CategoryData>; readonly actions: ReadonlyArray<CategoryData>;
readonly functions?: ReadonlyArray<FunctionData>; readonly functions?: ReadonlyArray<FunctionData>;
} }
export interface CategoryData extends DocumentableData { export interface CategoryData extends DocumentableData {
readonly children: ReadonlyArray<CategoryOrScriptData>; readonly children: ReadonlyArray<CategoryOrScriptData>;
readonly category: string; readonly category: string;
} }
export type CategoryOrScriptData = CategoryData | ScriptData; export type CategoryOrScriptData = CategoryData | ScriptData;
export type DocumentationUrlsData = ReadonlyArray<string> | string; export type DocumentationUrlsData = ReadonlyArray<string> | string;
export interface DocumentableData { export interface DocumentableData {
readonly docs?: DocumentationUrlsData; readonly docs?: DocumentationUrlsData;
} }
export interface InstructionHolder { export interface InstructionHolder {
readonly name: string; readonly name: string;
readonly code?: string; readonly code?: string;
readonly revertCode?: string; readonly revertCode?: string;
readonly call?: FunctionCallsData; readonly call?: FunctionCallsData;
} }
export interface ParameterDefinitionData { export interface ParameterDefinitionData {
readonly name: string; readonly name: string;
readonly optional?: boolean; readonly optional?: boolean;
} }
export interface FunctionData extends InstructionHolder { export interface FunctionData extends InstructionHolder {
readonly parameters?: readonly ParameterDefinitionData[]; readonly parameters?: readonly ParameterDefinitionData[];
} }
export interface FunctionCallParametersData { export interface FunctionCallParametersData {
readonly [index: string]: string; readonly [index: string]: string;
} }
export interface FunctionCallData { export interface FunctionCallData {
readonly function: string; readonly function: string;
readonly parameters?: FunctionCallParametersData; readonly parameters?: FunctionCallParametersData;
} }
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
export interface ScriptData extends InstructionHolder, DocumentableData { export interface ScriptData extends InstructionHolder, DocumentableData {
readonly name: string; readonly name: string;
readonly recommend?: string; readonly recommend?: string;
} }
export interface ScriptingDefinitionData { export interface ScriptingDefinitionData {
readonly language: string; readonly language: string;
readonly fileExtension: string; readonly fileExtension: string;
readonly startCode: string; readonly startCode: string;
readonly endCode: string; readonly endCode: string;
} }
const content: CollectionData; const content: CollectionData;
export default content; export default content;
} }

Some files were not shown because too many files have changed in this diff Show More