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

@@ -8,13 +8,16 @@ 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>; private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) { protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) { if (!costlyGetter) {
throw new Error('undefined getter'); throw new Error('undefined getter');
} }
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter())); this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
} }
public getApp(): Promise<IApplication> { public getApp(): Promise<IApplication> {
return this.getter.getValue(); return this.getter.getValue();
} }

View File

@@ -1,20 +1,25 @@
// 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>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> { enumVariable: EnumVariable<T, TEnumValue>,
): IEnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };
} }
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>( function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
value: string, value: string,
enumName: string, enumName: string,
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue { enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue {
if (!value) { if (!value) {
throw new Error(`undefined ${enumName}`); throw new Error(`undefined ${enumName}`);
} }
@@ -29,22 +34,26 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
return enumVariable[casedValue as keyof typeof enumVariable]; return enumVariable[casedValue as keyof typeof enumVariable];
} }
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>( export function getEnumNames
enumVariable: EnumVariable<T, TEnumValue>): string[] { <T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>,
): string[] {
return Object return Object
.values(enumVariable) .values(enumVariable)
.filter((enumMember) => typeof enumMember === 'string') as string[]; .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>,
): TEnumValue[] {
return getEnumNames(enumVariable) return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[]; .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) { if (value === undefined) {
throw new Error('undefined enum value'); throw new Error('undefined enum value');
} }

View File

@@ -1,6 +1,6 @@
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;
@@ -27,5 +27,4 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
} }
this.getters.set(language, getter); this.getters.set(language, getter);
} }
} }

View File

@@ -1,17 +1,19 @@
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 collection: ICategoryCollection;
public currentOs: OperatingSystem; public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState { public get state(): ICategoryCollectionState {
@@ -19,9 +21,11 @@ export class ApplicationContext implements IApplicationContext {
} }
private readonly states: StateMachine; private readonly states: StateMachine;
public constructor( public constructor(
public readonly app: IApplication, public readonly app: IApplication,
initialContext: OperatingSystem) { initialContext: OperatingSystem,
) {
validateApp(app); validateApp(app);
assertInRange(initialContext, OperatingSystem); assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app); this.states = initializeStates(app);

View File

@@ -1,15 +1,16 @@
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,
): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); } if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); } if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getApp(); const app = await factory.getApp();

View File

@@ -1,7 +1,7 @@
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;

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,13 +7,14 @@ 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 code: IApplicationCode;
public readonly selection: IUserSelection; public readonly selection: IUserSelection;
public readonly filter: IUserFilter; public readonly filter: IUserFilter;
public constructor(readonly collection: ICategoryCollection) { public constructor(readonly collection: ICategoryCollection) {

View File

@@ -1,16 +1,17 @@
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; public current: string;
private scriptPositions = new Map<SelectedScript, CodePosition>(); private scriptPositions = new Map<SelectedScript, CodePosition>();
@@ -18,7 +19,8 @@ export class ApplicationCode implements IApplicationCode {
constructor( constructor(
userSelection: IReadOnlyUserSelection, userSelection: IReadOnlyUserSelection,
private readonly scriptingDefinition: IScriptingDefinition, private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) { private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); } if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); } if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
if (!generator) { throw new Error('generator is null or undefined'); } if (!generator) { throw new Error('generator is null or undefined'); }

View File

@@ -1,12 +1,15 @@
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 addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>; public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>; public readonly changedScripts: ReadonlyArray<IScript>;
private readonly scripts: Map<IScript, ICodePosition>; private readonly scripts: Map<IScript, ICodePosition>;
@@ -14,7 +17,8 @@ export class CodeChangedEvent implements ICodeChangedEvent {
constructor( constructor(
code: string, code: string,
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>) { scripts: Map<SelectedScript, ICodePosition>,
) {
ensureAllPositionsExist(code, Array.from(scripts.values())); ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code; this.code = code;
const newScripts = Array.from(scripts.keys()); const newScripts = Array.from(scripts.keys());
@@ -40,15 +44,18 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
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>,
): ReadonlyArray<IScript> {
return newScripts return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert)) && oldScript.revert !== newScript.revert))
@@ -57,7 +64,8 @@ function getChangedScripts(
function selectIfNotExists( function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>, selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>) { test: ReadonlyArray<SelectedScript>,
) {
return selectableContainer return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id)) .filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script); .map((selection) => selection.script);

View File

@@ -24,7 +24,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
} }
public appendTrailingHyphensCommentLine( public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
} }
@@ -45,7 +46,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
public appendCommentLineWithHyphensAround( public appendCommentLineWithHyphensAround(
sectionName: string, sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); } if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) { if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName); return this.appendCommentLine(sectionName);
@@ -63,5 +65,6 @@ export abstract class CodeBuilder implements ICodeBuilder {
} }
protected abstract getCommentDelimiter(): string; protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string; protected abstract writeStandardOut(text: string): string;
} }

View File

@@ -5,7 +5,9 @@ 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
extends ScriptingLanguageFactory<ICodeBuilder>
implements ICodeBuilderFactory {
constructor() { constructor() {
super(); super();
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());

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

@@ -1,6 +1,6 @@
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(

View File

@@ -4,6 +4,7 @@ export class BatchBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '::'; return '::';
} }
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`; return `echo ${escapeForEcho(text)}`;
} }

View File

@@ -4,6 +4,7 @@ export class ShellBuilder extends CodeBuilder {
protected getCommentDelimiter(): string { protected getCommentDelimiter(): string {
return '#'; return '#';
} }
protected writeStandardOut(text: string): string { protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`; return `echo '${escapeForEcho(text)}'`;
} }

View File

@@ -1,9 +1,9 @@
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';
@@ -12,9 +12,11 @@ export class UserScriptGenerator implements IUserScriptGenerator {
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) { constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
} }
public buildCode( public buildCode(
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript { scriptingDefinition: IScriptingDefinition,
): IUserScript {
if (!selectedScripts) { throw new Error('undefined scripts'); } if (!selectedScripts) { throw new Error('undefined scripts'); }
if (!scriptingDefinition) { throw new Error('undefined definition'); } if (!scriptingDefinition) { throw new Error('undefined definition'); }
let scriptPositions = new Map<SelectedScript, ICodePosition>(); let scriptPositions = new Map<SelectedScript, ICodePosition>();
@@ -52,9 +54,11 @@ function finalizeCode(builder: ICodeBuilder, endCode: string): string {
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 startPosition = builder.currentLine + 1;
appendCode(selection, builder);
const endPosition = builder.currentLine - 1; const endPosition = builder.currentLine - 1;
builder.appendLine(); builder.appendLine();
const position = new CodePosition(startPosition, endPosition); const position = new CodePosition(startPosition, endPosition);
@@ -63,8 +67,9 @@ function appendSelection(
} }
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;
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
return builder return builder
.appendLine() .appendLine()
.appendFunction(name, scriptCode); .appendFunction(name, scriptCode);

View File

@@ -1,5 +1,5 @@
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>;

View File

@@ -7,7 +7,8 @@ export class CodePosition implements ICodePosition {
constructor( constructor(
public readonly startLine: number, public readonly startLine: number,
public readonly endLine: number) { public readonly endLine: number,
) {
if (startLine < 0) { if (startLine < 0) {
throw new Error('Code cannot start in a negative line'); throw new Error('Code cannot start in a negative line');
} }

View File

@@ -1,16 +1,18 @@
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 (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); } if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); } if (!categoryMatches) { throw new Error('Category matches is undefined'); }
} }
public hasAnyMatches(): boolean { public hasAnyMatches(): boolean {
return this.scriptMatches.length > 0 return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0; || this.categoryMatches.length > 0;

View File

@@ -1,13 +1,15 @@
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 readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined; public currentFilter: IFilterResult | undefined;
constructor(private collection: ICategoryCollection) { constructor(private collection: ICategoryCollection) {
@@ -20,9 +22,11 @@ export class UserFilter implements IUserFilter {
} }
const filterLowercase = filter.toLocaleLowerCase(); const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.collection.getAllScripts().filter( const filteredScripts = this.collection.getAllScripts().filter(
(script) => isScriptAMatch(script, filterLowercase)); (script) => isScriptAMatch(script, filterLowercase),
);
const filteredCategories = this.collection.getAllCategories().filter( const filteredCategories = this.collection.getAllCategories().filter(
(category) => category.name.toLowerCase().includes(filterLowercase)); (category) => category.name.toLowerCase().includes(filterLowercase),
);
const matches = new FilterResult( const matches = new FilterResult(
filteredScripts, filteredScripts,
filteredCategories, filteredCategories,

View File

@@ -1,8 +1,8 @@
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;

View File

@@ -1,7 +1,7 @@
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>>;

View File

@@ -1,19 +1,21 @@
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>; private readonly scripts: IRepository<string, SelectedScript>;
constructor( constructor(
private readonly collection: ICategoryCollection, private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>) { selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>(); this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) { if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) { for (const script of selectedScripts) {
@@ -30,7 +32,9 @@ export class UserSelection implements IUserSelection {
if (this.selectedScripts.length < scripts.length) { if (this.selectedScripts.length < scripts.length) {
return false; return false;
} }
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id)); return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
} }
public isAnySelected(category: ICategory): boolean { public isAnySelected(category: ICategory): boolean {
@@ -53,11 +57,11 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void { public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const category = this.collection.findCategory(categoryId); const category = this.collection.findCategory(categoryId);
const scriptsToAddOrUpdate = category.getAllScriptsRecursively() const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
.filter((script) => .filter(
!this.scripts.exists(script.id) (script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert, || this.scripts.getById(script.id).revert !== revert,
); );
if (!scriptsToAddOrUpdate.length) { if (!scriptsToAddOrUpdate.length) {

View File

@@ -4,6 +4,7 @@ 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 { public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) { if (!userAgent) {
return undefined; return undefined;
@@ -19,35 +20,37 @@ export class BrowserOsDetector implements IBrowserOsDetector {
} }
// 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(
os: OperatingSystem,
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
): IBrowserOsDetector {
const builder = new DetectorBuilder(os); const builder = new DetectorBuilder(os);
applyRules(builder); applyRules(builder);
return builder.build(); return builder.build();

View File

@@ -1,8 +1,9 @@
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>(); private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { } constructor(private readonly os: OperatingSystem) { }

View File

@@ -1,9 +1,9 @@
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;
@@ -15,11 +15,15 @@ export class Environment implements IEnvironment {
process: typeof process !== 'undefined' ? process /* electron only */ : undefined, process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
navigator, navigator,
}); });
public readonly isDesktop: boolean; public readonly isDesktop: boolean;
public readonly os: OperatingSystem; public readonly os: OperatingSystem;
protected constructor( protected constructor(
variables: IEnvironmentVariables, variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) { browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
) {
if (!variables) { if (!variables) {
throw new Error('variables is null or empty'); throw new Error('variables is null or empty');
} }
@@ -49,15 +53,17 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
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) {
case 'darwin':
return OperatingSystem.macOS; return OperatingSystem.macOS;
} else if (processPlatform === 'win32') { case 'win32':
return OperatingSystem.Windows; return OperatingSystem.Windows;
} else if (processPlatform === 'linux') { case 'linux':
return OperatingSystem.Linux; return OperatingSystem.Linux;
} default:
return undefined; 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

View File

@@ -1,17 +1,18 @@
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,
): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData);
const information = parseProjectInformation(processEnv); const information = parseProjectInformation(processEnv);
const collections = collectionsData.map((collection) => parser(collection, information)); const collections = collectionsData.map((collection) => parser(collection, information));
@@ -22,11 +23,13 @@ export function parseApplication(
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) {

View File

@@ -1,18 +1,19 @@
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),
): ICategoryCollection {
validate(content); validate(content);
const scripting = new ScriptingDefinitionParser() const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info); .parse(content.scripting, info);
@@ -26,7 +27,8 @@ export function parseCategoryCollection(
const collection = new CategoryCollection( const collection = new CategoryCollection(
os, os,
categories, categories,
scripting); scripting,
);
return collection; return collection;
} }

View File

@@ -1,18 +1,23 @@
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(
category: CategoryData,
context: ICategoryCollectionParseContext,
): Category {
if (!context) { throw new Error('undefined context'); } if (!context) { throw new Error('undefined context'); }
ensureValid(category); ensureValid(category);
const children: ICategoryChildren = { const children: ICategoryChildren = {
@@ -23,11 +28,11 @@ export function parseCategory(category: CategoryData, context: ICategoryCollecti
parseCategoryChild(data, children, category, context); parseCategoryChild(data, children, category, context);
} }
return new Category( return new Category(
/*id*/ categoryIdCounter++, /* id: */ categoryIdCounter++,
/*name*/ category.category, /* name: */ category.category,
/*docs*/ parseDocUrls(category), /* docs: */ parseDocUrls(category),
/*categories*/ children.subCategories, /* categories: */ children.subCategories,
/*scripts*/ children.subScripts, /* scripts: */ children.subScripts,
); );
} }
@@ -47,7 +52,8 @@ function parseCategoryChild(
data: CategoryOrScriptData, data: CategoryOrScriptData,
children: ICategoryChildren, children: ICategoryChildren,
parent: CategoryData, parent: CategoryData,
context: ICategoryCollectionParseContext) { context: ICategoryCollectionParseContext,
) {
if (isCategory(data)) { if (isCategory(data)) {
const subCategory = parseCategory(data as CategoryData, context); const subCategory = parseCategory(data as CategoryData, context);
children.subCategories.push(subCategory); children.subCategories.push(subCategory);
@@ -61,11 +67,20 @@ function parseCategoryChild(
} }
} }
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

@@ -4,7 +4,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
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 [];
} }
@@ -13,7 +13,10 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
return result.getAll(); return result.getAll();
} }
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer { function addDocs(
docs: DocumentationUrlsData,
urls: DocumentationUrlContainer,
): DocumentationUrlContainer {
if (docs instanceof Array) { if (docs instanceof Array) {
urls.addUrls(docs); urls.addUrls(docs);
} else if (typeof docs === 'string') { } else if (typeof docs === 'string') {
@@ -32,7 +35,7 @@ class DocumentationUrlContainer {
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');
@@ -53,8 +56,8 @@ function validateUrl(docUrl: string): void {
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,7 +2,8 @@ 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,
): IProjectInformation {
return new ProjectInformation( return new ProjectInformation(
environment.VUE_APP_NAME, environment.VUE_APP_NAME,
environment.VUE_APP_VERSION, environment.VUE_APP_VERSION,

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';
@@ -9,12 +9,14 @@ 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; public readonly syntax: ILanguageSyntax;
constructor( constructor(
functionsData: ReadonlyArray<FunctionData> | undefined, functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition, scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) { syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
if (!scripting) { throw new Error('undefined scripting'); } if (!scripting) { throw new Error('undefined scripting'); }
this.syntax = syntaxFactory.create(scripting.language); this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax); this.compiler = new ScriptCompiler(functionsData, this.syntax);

View File

@@ -1,18 +1,19 @@
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
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
) {
if (!position) { if (!position) {
throw new Error('undefined position'); throw new Error('undefined position');
} }
@@ -20,14 +21,15 @@ export class Expression implements IExpression {
throw new Error('undefined evaluator'); throw new Error('undefined evaluator');
} }
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
if (!context) { if (!context) {
throw new Error('undefined context'); throw new Error('undefined context');
} }
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args);
context = new ExpressionEvaluationContext(args, context.pipelineCompiler); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(context); return this.evaluator(filteredContext);
} }
} }
@@ -43,20 +45,19 @@ function validateThatAllRequiredParametersAreSatisfied(
.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,
): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection(); const specificCallArgs = new FunctionCallArgumentCollection();
for (const parameter of parameters.all) { parameters.all
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) { .filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
continue; // Optional parameter is not necessarily provided .map((parameter) => allFunctionArgs.getArgument(parameter.name))
} .forEach((argument) => specificCallArgs.addArgument(argument));
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
}
return specificCallArgs; return specificCallArgs;
} }

View File

@@ -10,7 +10,8 @@ export interface IExpressionEvaluationContext {
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) { if (!args) {
throw new Error('undefined args, send empty collection instead'); throw new Error('undefined args, send empty collection instead');
} }

View File

@@ -1,7 +1,8 @@
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) { if (start === end) {
throw new Error(`no length (start = end = ${start})`); throw new Error(`no length (start = end = ${start})`);
} }

View File

@@ -1,5 +1,5 @@
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 {

View File

@@ -1,17 +1,19 @@
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( public compileExpressions(
code: string | undefined, code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string { args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) { if (!args) {
throw new Error('undefined args, send empty collection instead'); throw new Error('undefined args, send empty collection instead');
} }
@@ -29,7 +31,8 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
function compileExpressions( function compileExpressions(
expressions: readonly IExpression[], expressions: readonly IExpression[],
code: string, code: string,
context: IExpressionEvaluationContext) { context: IExpressionEvaluationContext,
) {
let compiledCode = ''; let compiledCode = '';
const sortedExpressions = expressions const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter .slice() // copy the array to not mutate the parameter
@@ -51,7 +54,8 @@ function compileExpressions(
} }
function extractRequiredParameterNames( function extractRequiredParameterNames(
expressions: readonly IExpression[]): string[] { expressions: readonly IExpression[],
): string[] {
const usedParameterNames = expressions const usedParameterNames = expressions
.map((e) => e.parameters.all .map((e) => e.parameters.all
.filter((p) => !p.isOptional) .filter((p) => !p.isOptional)
@@ -64,7 +68,8 @@ function extractRequiredParameterNames(
function ensureParamsUsedInCodeHasArgsProvided( function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[], expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void { providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions); const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) { if (!usedParameterNames?.length) {
return; return;

View File

@@ -1,7 +1,7 @@
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(),
@@ -14,6 +14,7 @@ export class CompositeExpressionParser implements IExpressionParser {
throw new Error('undefined leaf'); throw new Error('undefined leaf');
} }
} }
public findExpressions(code: string): IExpression[] { public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>(); const expressions = new Array<IExpression>();
for (const parser of this.leafs) { for (const parser of this.leafs) {

View File

@@ -52,6 +52,7 @@ export class ExpressionRegexBuilder {
return this return this
.addRawRegex('\\s*'); .addRawRegex('\\s*');
} }
private addRawRegex(regex: string) { private addRawRegex(regex: string) {
this.parts.push(regex); this.parts.push(regex);
return this; return this;

View File

@@ -42,7 +42,8 @@ export interface IPrimitiveExpression {
} }
function getParameters( function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection { expression: IPrimitiveExpression,
): FunctionParameterCollection {
const parameters = new FunctionParameterCollection(); const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) { for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter); parameters.addParameter(parameter);

View File

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

View File

@@ -2,18 +2,19 @@ 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 { public apply(code: string): string {
if (!code || !hasLines(code)) { if (!code || !hasLines(code)) {
return code; return code;
} }
code = inlineComments(code); const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
code = mergeLinesWithBacktick(code); inlineComments,
code = mergeHereStrings(code); mergeLinesWithBacktick,
const lines = getLines(code) mergeHereStrings,
.map((line) => line.trim()) mergeNewLines,
.filter((line) => line.length > 0); ]).reduce((a, b) => (data) => b(a(data)));
return lines const newCode = processor(code);
.join('; '); return newCode;
} }
} }
@@ -112,15 +113,18 @@ interface IInlinedHereString {
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 {
/*
We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable.
*/
const expandableNewLine = '`r`n'; const expandableNewLine = '`r`n';
switch (quotes) { switch (quotes) {
case '\'': case '\'':
return { return {
quotesAround: '\'', quotesAround: '\'',
escapedQuotes: '\'\'', escapedQuotes: '\'\'',
separator: `\'+"${expandableNewLine}"+\'`, separator: `'+"${expandableNewLine}"+'`,
}; };
case '"': case '"':
return { return {
@@ -153,3 +157,10 @@ function mergeLinesWithBacktick(code: string) {
*/ */
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

@@ -13,6 +13,7 @@ export interface IPipeFactory {
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) { constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) { if (pipes.some((pipe) => !pipe)) {
throw new Error('undefined pipe in list'); throw new Error('undefined pipe in list');
@@ -21,6 +22,7 @@ export class PipeFactory implements IPipeFactory {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
} }
public get(pipeName: string): IPipe { public get(pipeName: string): IPipe {
validatePipeName(pipeName); validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) { if (!this.pipes.has(pipeName)) {
@@ -28,6 +30,7 @@ export class PipeFactory implements IPipeFactory {
} }
return this.pipes.get(pipeName); return this.pipes.get(pipeName);
} }
private registerPipe(pipe: IPipe): void { private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name); validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) { if (this.pipes.has(pipe.name)) {

View File

@@ -3,14 +3,16 @@ 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 { public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline); ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline); const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName)); const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
let valueInCompilation = value;
for (const pipe of pipes) { for (const pipe of pipes) {
value = pipe.apply(value); valueInCompilation = pipe.apply(valueInCompilation);
} }
return value; return valueInCompilation;
} }
} }

View File

@@ -1,5 +1,5 @@
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 {
@@ -17,7 +17,7 @@ export class ParameterSubstitutionParser extends RegexParser {
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;
} }

View File

@@ -1,5 +1,5 @@
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 {
@@ -25,8 +25,8 @@ export class WithParser extends RegexParser {
return { return {
parameters: [new FunctionParameter(parameterName, true)], parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => { evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName) ? const argumentValue = context.args.hasArgument(parameterName)
context.args.getArgument(parameterName).argumentValue ? context.args.getArgument(parameterName).argumentValue
: undefined; : undefined;
if (!argumentValue) { if (!argumentValue) {
return ''; return '';
@@ -51,7 +51,8 @@ const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
.buildRegExp(); .buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) { function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those // 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 scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1); return replacer(match1);
}); });

View File

@@ -1,10 +1,11 @@
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); ensureValidParameterName(parameterName);
if (!argumentValue) { if (!argumentValue) {
throw new Error(`undefined argument value for "${parameterName}"`); throw new Error(`undefined argument value for "${parameterName}"`);

View File

@@ -3,6 +3,7 @@ import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollecti
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 { public addArgument(argument: IFunctionCallArgument): void {
if (!argument) { if (!argument) {
throw new Error('undefined argument'); throw new Error('undefined argument');
@@ -12,15 +13,18 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
} }
this.arguments.set(argument.parameterName, argument); this.arguments.set(argument.parameterName, argument);
} }
public getAllParameterNames(): string[] { public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys()); return Array.from(this.arguments.keys());
} }
public hasArgument(parameterName: string): boolean { public hasArgument(parameterName: string): boolean {
if (!parameterName) { if (!parameterName) {
throw new Error('undefined parameter name'); throw new Error('undefined parameter name');
} }
return this.arguments.has(parameterName); return this.arguments.has(parameterName);
} }
public getArgument(parameterName: string): IFunctionCallArgument { public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) { if (!parameterName) {
throw new Error('undefined parameter name'); throw new Error('undefined parameter name');

View File

@@ -1,25 +1,27 @@
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,
): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); } if (!functions) { throw new Error('undefined functions'); }
if (!calls) { throw new Error('undefined calls'); } if (!calls) { throw new Error('undefined calls'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); } if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
@@ -56,13 +58,17 @@ function compileCallSequence(context: ICompilationContext): ICompiledFunctionCal
}; };
} }
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] { function compileSingleCall(
call: IFunctionCall,
context: ICompilationContext,
): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName); const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args); ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler); const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [compiledCode]; return [compiledCode];
} else { // Function with inner calls }
// Function with inner calls
return func.body.calls return func.body.calls
.map((innerCall) => { .map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler); const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
@@ -71,12 +77,12 @@ function compileSingleCall(call: IFunctionCall, context: ICompilationContext): I
}) })
.flat(); .flat();
} }
}
function compileCode( function compileCode(
code: IFunctionCode, code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunctionCall { compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return { return {
code: compiler.compileExpressions(code.do, args), code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args), revertCode: compiler.compileExpressions(code.revert, args),
@@ -90,7 +96,7 @@ function compileArgs(
): 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);
@@ -106,7 +112,8 @@ function merge(codeParts: readonly string[]): string {
function ensureThatCallArgumentsExistInParameterDefinition( function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction, func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void { args: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = args.getAllParameterNames(); const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || []; const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
@@ -115,7 +122,8 @@ function ensureThatCallArgumentsExistInParameterDefinition(
function findUnexpectedParameters( function findUnexpectedParameters(
callArgumentNames: string[], callArgumentNames: string[],
functionParameterNames: string[]): string[] { functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) { if (!callArgumentNames.length && !functionParameterNames.length) {
return []; return [];
} }
@@ -126,14 +134,16 @@ function findUnexpectedParameters(
function throwIfNotEmpty( function throwIfNotEmpty(
functionName: string, functionName: string,
unexpectedParameters: string[], unexpectedParameters: string[],
expectedParameters: string[]) { expectedParameters: string[],
) {
if (!unexpectedParameters.length) { if (!unexpectedParameters.length) {
return; return;
} }
throw new Error( throw new Error(
`Function "${functionName}" has unexpected parameter(s) provided: ` + // eslint-disable-next-line prefer-template
`"${unexpectedParameters.join('", "')}"` + `Function "${functionName}" has unexpected parameter(s) provided: `
'. Expected parameter(s): ' + + `"${unexpectedParameters.join('", "')}"`
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
); );
} }

View File

@@ -1,6 +1,6 @@
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(

View File

@@ -4,7 +4,8 @@ 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) { if (!functionName) {
throw new Error('empty function name in function call'); throw new Error('empty function name in function call');
} }

View File

@@ -5,7 +5,7 @@ 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);
@@ -24,7 +24,7 @@ function getCallSequence(calls: FunctionCallsData): 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 || {})) {

View File

@@ -1,5 +1,5 @@
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;

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

@@ -7,6 +7,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
public get all(): readonly IFunctionParameter[] { public get all(): readonly IFunctionParameter[] {
return this.parameters; return this.parameters;
} }
public addParameter(parameter: IFunctionParameter) { public addParameter(parameter: IFunctionParameter) {
this.ensureValidParameter(parameter); this.ensureValidParameter(parameter);
this.parameters.push(parameter); this.parameters.push(parameter);
@@ -15,6 +16,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
private includesName(name: string) { private includesName(name: string) {
return this.parameters.find((existingParameter) => existingParameter.name === name); return this.parameters.find((existingParameter) => existingParameter.name === name);
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) { if (!parameter) {
throw new Error('undefined parameter'); throw new Error('undefined parameter');

View File

@@ -1,11 +1,14 @@
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[],
): ISharedFunction {
if (!callSequence) { if (!callSequence) {
throw new Error(`undefined call sequence in function "${name}"`); throw new Error(`undefined call sequence in function "${name}"`);
} }
@@ -19,7 +22,8 @@ export function createFunctionWithInlineCode(
name: string, name: string,
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
code: string, code: string,
revertCode?: string): ISharedFunction { revertCode?: string,
): ISharedFunction {
if (!code) { if (!code) {
throw new Error(`undefined code in function "${name}"`); throw new Error(`undefined code in function "${name}"`);
} }
@@ -32,6 +36,7 @@ export function createFunctionWithInlineCode(
class SharedFunction implements ISharedFunction { class SharedFunction implements ISharedFunction {
public readonly body: ISharedFunctionBody; public readonly body: ISharedFunctionBody;
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly parameters: IReadOnlyFunctionParameterCollection, public readonly parameters: IReadOnlyFunctionParameterCollection,
@@ -39,7 +44,7 @@ class SharedFunction implements ISharedFunction {
bodyType: FunctionBodyType, bodyType: FunctionBodyType,
) { ) {
if (!name) { throw new Error('undefined function name'); } if (!name) { throw new Error('undefined function name'); }
if (!parameters) { throw new Error(`undefined parameters`); } if (!parameters) { throw new Error('undefined parameters'); }
this.body = { this.body = {
type: bodyType, type: bodyType,
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,

View File

@@ -11,8 +11,10 @@ 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( public parseFunctions(
functions: readonly FunctionData[]): ISharedFunctionCollection { functions: readonly FunctionData[],
): ISharedFunctionCollection {
const collection = new SharedFunctionCollection(); const collection = new SharedFunctionCollection();
if (!functions || !functions.length) { if (!functions || !functions.length) {
return collection; return collection;
@@ -27,15 +29,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
} }
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 }
// Has call
const calls = parseFunctionCalls(data.call); const calls = parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls); return createCallerFunction(name, parameters, calls);
} }
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection(); const parameters = new FunctionParameterCollection();
@@ -96,7 +98,7 @@ function ensureExpectedParametersType(functions: readonly FunctionData[]) {
} }
} }
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');
} }
@@ -115,15 +117,14 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
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)}`);
} }

View File

@@ -1,5 +1,5 @@
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;

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';
@@ -12,15 +11,17 @@ 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( constructor(
functions: readonly FunctionData[] | undefined, functions: readonly FunctionData[] | undefined,
private readonly syntax: ILanguageSyntax, private readonly syntax: ILanguageSyntax,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
) { ) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
this.functions = sharedFunctionsParser.parseFunctions(functions); this.functions = sharedFunctionsParser.parseFunctions(functions);
} }
public canCompile(script: ScriptData): boolean { public canCompile(script: ScriptData): boolean {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
if (!script.call) { if (!script.call) {
@@ -28,6 +29,7 @@ export class ScriptCompiler implements IScriptCompiler {
} }
return true; return true;
} }
public compile(script: ScriptData): IScriptCode { public compile(script: ScriptData): IScriptCode {
if (!script) { throw new Error('undefined script'); } if (!script) { throw new Error('undefined script'); }
try { try {
@@ -36,7 +38,8 @@ export class ScriptCompiler implements IScriptCompiler {
return new ScriptCode( return new ScriptCode(
compiledCode.code, compiledCode.code,
compiledCode.revertCode, compiledCode.revertCode,
this.syntax); this.syntax,
);
} catch (error) { } catch (error) {
throw Error(`Script "${script.name}" ${error.message}`); throw Error(`Script "${script.name}" ${error.message}`);
} }

View File

@@ -1,26 +1,32 @@
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,
levelParser = createEnumParser(RecommendationLevel),
): Script {
validateScript(data); validateScript(data);
if (!context) { throw new Error('undefined context'); } if (!context) { throw new Error('undefined context'); }
const script = new Script( const script = new Script(
/* name */ data.name, /* name: */ data.name,
/* code */ parseCode(data, context), /* code: */ parseCode(data, context),
/* docs */ parseDocUrls(data), /* docs: */ parseDocUrls(data),
/* level */ parseLevel(data.recommend, levelParser)); /* level: */ parseLevel(data.recommend, levelParser),
);
return script; return script;
} }
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined { function parseLevel(
level: string,
parser: IEnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) { if (!level) {
return undefined; return undefined;
} }

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

@@ -2,5 +2,6 @@ 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,7 +5,9 @@ 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
extends ScriptingLanguageFactory<ILanguageSyntax>
implements ISyntaxFactory {
constructor() { constructor() {
super(); super();
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax()); this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());

View File

@@ -3,9 +3,9 @@ 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(
@@ -14,12 +14,13 @@ export class CodeSubstituter implements ICodeSubstituter {
) { ) {
} }
public substitute(code: string, info: IProjectInformation): string { public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('undefined code'); } if (!code) { throw new Error('undefined code'); }
if (!info) { throw new Error('undefined info'); } if (!info) { throw new Error('undefined info'); }
const args = new FunctionCallArgumentCollection(); const args = new FunctionCallArgumentCollection();
const substitute = (name: string, value: string) => const substitute = (name: string, value: string) => args
args.addArgument(new FunctionCallArgument(name, value)); .addArgument(new FunctionCallArgument(name, value));
substitute('homepage', info.homepage); substitute('homepage', info.homepage);
substitute('version', info.version); substitute('version', info.version);
substitute('date', this.date.toUTCString()); substitute('date', this.date.toUTCString());

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';
@@ -13,9 +13,11 @@ export class ScriptingDefinitionParser {
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(), private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
) { ) {
} }
public parse( public parse(
definition: ScriptingDefinitionData, definition: ScriptingDefinitionData,
info: IProjectInformation): IScriptingDefinition { info: IProjectInformation,
): IScriptingDefinition {
if (!info) { throw new Error('undefined info'); } if (!info) { throw new Error('undefined info'); }
if (!definition) { throw new Error('undefined definition'); } if (!definition) { throw new Error('undefined definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language'); const language = this.languageParser.parseEnum(definition.language, 'language');
@@ -28,4 +30,3 @@ export class ScriptingDefinitionParser {
); );
} }
} }

View File

@@ -955,7 +955,7 @@ actions:
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no' sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
echo "Disabled Gatekeeper" echo "Disabled Gatekeeper"
else else
>&2 echo "Unknown gatekeeper status: $gatekeeper_status" >&2 echo "Unknown gatekeeper status: $gatekeeper_status"
fi fi
fi fi
revertCode: |- revertCode: |-
@@ -974,7 +974,7 @@ actions:
elif [ $gatekeeper_status = "enabled" ]; then elif [ $gatekeeper_status = "enabled" ]; then
echo "No action needed, Gatekeeper is already enabled" echo "No action needed, Gatekeeper is already enabled"
else else
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status" >&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
fi fi
fi fi
- -

View File

@@ -4,7 +4,10 @@ import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
export class Application implements IApplication { export class Application implements IApplication {
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) { constructor(
public info: IProjectInformation,
public collections: readonly ICategoryCollection[],
) {
validateInformation(info); validateInformation(info);
validateCollections(collections); validateCollections(collections);
} }
@@ -37,8 +40,8 @@ function validateCollections(collections: readonly ICategoryCollection[]) {
const osList = collections.map((c) => c.os); const osList = collections.map((c) => c.os);
const duplicates = getDuplicates(osList); const duplicates = getDuplicates(osList);
if (duplicates.length > 0) { if (duplicates.length > 0) {
throw new Error('multiple collections with same os: ' + throw new Error(`multiple collections with same os: ${
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')); duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`);
} }
} }

View File

@@ -10,7 +10,8 @@ export class Category extends BaseEntity<number> implements ICategory {
public readonly name: string, public readonly name: string,
public readonly documentationUrls: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly subCategories?: ReadonlyArray<ICategory>, public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>) { public readonly scripts?: ReadonlyArray<IScript>,
) {
super(id); super(id);
validateCategory(this); validateCategory(this);
} }
@@ -20,7 +21,10 @@ export class Category extends BaseEntity<number> implements ICategory {
} }
public getAllScriptsRecursively(): readonly IScript[] { public getAllScriptsRecursively(): readonly IScript[] {
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this)); if (!this.allSubScripts) {
this.allSubScripts = parseScriptsRecursively(this);
}
return this.allSubScripts;
} }
} }
@@ -35,8 +39,10 @@ function validateCategory(category: ICategory) {
if (!category.name) { if (!category.name) {
throw new Error('undefined or empty name'); throw new Error('undefined or empty name');
} }
if ((!category.subCategories || category.subCategories.length === 0) && if (
(!category.scripts || category.scripts.length === 0)) { (!category.subCategories || category.subCategories.length === 0)
&& (!category.scripts || category.scripts.length === 0)
) {
throw new Error('A category must have at least one sub-category or script'); throw new Error('A category must have at least one sub-category or script');
} }
} }

View File

@@ -9,6 +9,7 @@ import { ICategoryCollection } from './ICategoryCollection';
export class CategoryCollection implements ICategoryCollection { export class CategoryCollection implements ICategoryCollection {
public get totalScripts(): number { return this.queryable.allScripts.length; } public get totalScripts(): number { return this.queryable.allScripts.length; }
public get totalCategories(): number { return this.queryable.allCategories.length; } public get totalCategories(): number { return this.queryable.allCategories.length; }
private readonly queryable: IQueryableCollection; private readonly queryable: IQueryableCollection;
@@ -16,7 +17,8 @@ export class CategoryCollection implements ICategoryCollection {
constructor( constructor(
public readonly os: OperatingSystem, public readonly os: OperatingSystem,
public readonly actions: ReadonlyArray<ICategory>, public readonly actions: ReadonlyArray<ICategory>,
public readonly scripting: IScriptingDefinition) { public readonly scripting: IScriptingDefinition,
) {
if (!scripting) { if (!scripting) {
throw new Error('undefined scripting definition'); throw new Error('undefined scripting definition');
} }
@@ -32,7 +34,7 @@ export class CategoryCollection implements ICategoryCollection {
} }
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
if (isNaN(level)) { if (level === undefined) {
throw new Error('undefined level'); throw new Error('undefined level');
} }
if (!(level in RecommendationLevel)) { if (!(level in RecommendationLevel)) {
@@ -68,7 +70,8 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
if (duplicatedIds.length > 0) { if (duplicatedIds.length > 0) {
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
throw new Error( throw new Error(
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`); `Duplicate entities are detected with following id(s): ${duplicatedIdsText}`,
);
} }
} }
@@ -113,7 +116,8 @@ function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[],
function flattenCategories( function flattenCategories(
categories: ReadonlyArray<ICategory>, categories: ReadonlyArray<ICategory>,
allCategories: ICategory[], allCategories: ICategory[],
allScripts: IScript[]): IQueryableCollection { allScripts: IScript[],
): IQueryableCollection {
if (!categories || categories.length === 0) { if (!categories || categories.length === 0) {
return; return;
} }
@@ -126,7 +130,8 @@ function flattenCategories(
function flattenScripts( function flattenScripts(
scripts: ReadonlyArray<IScript>, scripts: ReadonlyArray<IScript>,
allScripts: IScript[]): IScript[] { allScripts: IScript[],
): IScript[] {
if (!scripts) { if (!scripts) {
return; return;
} }
@@ -136,7 +141,8 @@ function flattenScripts(
} }
function makeQueryable( function makeQueryable(
actions: ReadonlyArray<ICategory>): IQueryableCollection { actions: ReadonlyArray<ICategory>,
): IQueryableCollection {
const flattened = flattenApplication(actions); const flattened = flattenApplication(actions);
return { return {
allCategories: flattened[0], allCategories: flattened[0],
@@ -145,11 +151,15 @@ function makeQueryable(
}; };
} }
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> { function groupByLevel(
allScripts: readonly IScript[],
): Map<RecommendationLevel, readonly IScript[]> {
const map = new Map<RecommendationLevel, readonly IScript[]>(); const map = new Map<RecommendationLevel, readonly IScript[]>();
for (const levelName of getEnumNames(RecommendationLevel)) { for (const levelName of getEnumNames(RecommendationLevel)) {
const level = RecommendationLevel[levelName]; const level = RecommendationLevel[levelName];
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level); const scripts = allScripts.filter(
(script) => script.level !== undefined && script.level <= level,
);
map.set(level, scripts); map.set(level, scripts);
} }
return map; return map;

View File

@@ -1,4 +1,5 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
export interface IProjectInformation { export interface IProjectInformation {
readonly name: string; readonly name: string;
readonly version: string; readonly version: string;

View File

@@ -1,9 +1,10 @@
import { assertInRange } from '@/application/Common/Enum';
import { IProjectInformation } from './IProjectInformation'; import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from './OperatingSystem';
import { assertInRange } from '@/application/Common/Enum';
export class ProjectInformation implements IProjectInformation { export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string; public readonly repositoryWebUrl: string;
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly version: string, public readonly version: string,
@@ -24,12 +25,15 @@ export class ProjectInformation implements IProjectInformation {
} }
this.repositoryWebUrl = getWebUrl(this.repositoryUrl); this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
} }
public getDownloadUrl(os: OperatingSystem): string { public getDownloadUrl(os: OperatingSystem): string {
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`; return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
} }
public get feedbackUrl(): string { public get feedbackUrl(): string {
return `${this.repositoryWebUrl}/issues`; return `${this.repositoryWebUrl}/issues`;
} }
public get releaseUrl(): string { public get releaseUrl(): string {
return `${this.repositoryWebUrl}/releases/tag/${this.version}`; return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
} }

View File

@@ -8,13 +8,15 @@ export class Script extends BaseEntity<string> implements IScript {
public readonly name: string, public readonly name: string,
public readonly code: IScriptCode, public readonly code: IScriptCode,
public readonly documentationUrls: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly level?: RecommendationLevel) { public readonly level?: RecommendationLevel,
) {
super(name); super(name);
if (!code) { if (!code) {
throw new Error(`undefined code (script: ${name})`); throw new Error(`undefined code (script: ${name})`);
} }
validateLevel(level); validateLevel(level);
} }
public canRevert(): boolean { public canRevert(): boolean {
return Boolean(this.code.revert); return Boolean(this.code.revert);
} }

View File

@@ -4,7 +4,8 @@ export class ScriptCode implements IScriptCode {
constructor( constructor(
public readonly execute: string, public readonly execute: string,
public readonly revert: string, public readonly revert: string,
syntax: ILanguageSyntax) { syntax: ILanguageSyntax,
) {
if (!syntax) { throw new Error('undefined syntax'); } if (!syntax) { throw new Error('undefined syntax'); }
validateCode(execute, syntax); validateCode(execute, syntax);
validateRevertCode(revert, execute, syntax); validateRevertCode(revert, execute, syntax);
@@ -23,7 +24,7 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
try { try {
validateCode(revertCode, syntax); validateCode(revertCode, syntax);
if (execute === revertCode) { if (execute === revertCode) {
throw new Error(`Code itself and its reverting code cannot be the same`); throw new Error('Code itself and its reverting code cannot be the same');
} }
} catch (err) { } catch (err) {
throw Error(`(revert): ${err.message}`); throw Error(`(revert): ${err.message}`);
@@ -32,7 +33,7 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
function validateCode(code: string, syntax: ILanguageSyntax): void { function validateCode(code: string, syntax: ILanguageSyntax): void {
if (!code || code.length === 0) { if (!code || code.length === 0) {
throw new Error(`code is empty or undefined`); throw new Error('code is empty or undefined');
} }
ensureNoEmptyLines(code); ensureNoEmptyLines(code);
ensureCodeHasUniqueLines(code, syntax); ensureCodeHasUniqueLines(code, syntax);
@@ -61,7 +62,7 @@ function printDuplicatedLines(allLines: string[]) {
return allLines return allLines
.map((line, index) => { .map((line, index) => {
const occurrenceIndices = allLines const occurrenceIndices = allLines
.map((e, i) => e === line ? i : '') .map((e, i) => (e === line ? i : ''))
.filter(String); .filter(String);
const isDuplicate = occurrenceIndices.length > 1; const isDuplicate = occurrenceIndices.length > 1;
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ '; const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
@@ -71,10 +72,12 @@ function printDuplicatedLines(allLines: string[]) {
} }
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean { function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
codeLine = codeLine.toLowerCase(); const lowerCaseCodeLine = codeLine.toLowerCase();
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter)); const isCommentLine = () => syntax.commentDelimiters.some(
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
);
const consistsOfFrequentCommands = () => { const consistsOfFrequentCommands = () => {
const trimmed = codeLine.trim().split(' '); const trimmed = lowerCaseCodeLine.trim().split(' ');
return trimmed.every((part) => syntax.commonCodeParts.includes(part)); return trimmed.every((part) => syntax.commonCodeParts.includes(part));
}; };
return isCommentLine() || consistsOfFrequentCommands(); return isCommentLine() || consistsOfFrequentCommands();

View File

@@ -3,6 +3,7 @@ import { IScriptingDefinition } from './IScriptingDefinition';
export class ScriptingDefinition implements IScriptingDefinition { export class ScriptingDefinition implements IScriptingDefinition {
public readonly fileExtension: string; public readonly fileExtension: string;
constructor( constructor(
public readonly language: ScriptingLanguage, public readonly language: ScriptingLanguage,
public readonly startCode: string, public readonly startCode: string,

View File

@@ -1,15 +1,18 @@
import { Environment } from '@/application/Environment/Environment';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
// eslint-disable-next-line camelcase
import child_process from 'child_process'; import child_process from 'child_process';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export class CodeRunner { export class CodeRunner {
constructor( constructor(
private readonly node = getNodeJs(), private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment) { private readonly environment = Environment.CurrentEnvironment,
) {
} }
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> { public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName); const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, { recursive: true }); await this.node.fs.promises.mkdir(dir, { recursive: true });
@@ -36,13 +39,16 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
} }
function getNodeJs(): INodeJs { function getNodeJs(): INodeJs {
return { os, path, fs, child_process }; return {
os, path, fs, child_process,
};
} }
export interface INodeJs { export interface INodeJs {
os: INodeOs; os: INodeOs;
path: INodePath; path: INodePath;
fs: INodeFs; fs: INodeFs;
// eslint-disable-next-line camelcase
child_process: INodeChildProcess; child_process: INodeChildProcess;
} }

View File

@@ -2,9 +2,11 @@ import { IEventSubscription } from './IEventSource';
export class EventSubscriptionCollection { export class EventSubscriptionCollection {
private readonly subscriptions = new Array<IEventSubscription>(); private readonly subscriptions = new Array<IEventSubscription>();
public register(...subscriptions: IEventSubscription[]) { public register(...subscriptions: IEventSubscription[]) {
this.subscriptions.push(...subscriptions); this.subscriptions.push(...subscriptions);
} }
public unsubscribeAll() { public unsubscribeAll() {
this.subscriptions.forEach((listener) => listener.unsubscribe()); this.subscriptions.forEach((listener) => listener.unsubscribe());
this.subscriptions.splice(0, this.subscriptions.length); this.subscriptions.splice(0, this.subscriptions.length);

View File

@@ -7,5 +7,3 @@ export interface IEventSubscription {
} }
export type EventHandler<T> = (data: T) => void; export type EventHandler<T> = (data: T) => void;

View File

@@ -1,7 +1,8 @@
import { IEntity } from '../Entity/IEntity'; import { IEntity } from '../Entity/IEntity';
import { IRepository } from './IRepository'; import { IRepository } from './IRepository';
export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements IRepository<TKey, TEntity> { export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>>
implements IRepository<TKey, TEntity> {
private readonly items: TEntity[]; private readonly items: TEntity[];
constructor(items?: TEntity[]) { constructor(items?: TEntity[]) {

View File

@@ -4,11 +4,13 @@ export enum FileType {
BatchFile, BatchFile,
ShellScript, ShellScript,
} }
export class SaveFileDialog { export class SaveFileDialog {
public static saveFile(text: string, fileName: string, type: FileType): void { public static saveFile(text: string, fileName: string, type: FileType): void {
const mimeType = this.mimeTypes.get(type); const mimeType = this.mimeTypes.get(type);
this.saveBlob(text, mimeType, fileName); this.saveBlob(text, mimeType, fileName);
} }
private static readonly mimeTypes = new Map<FileType, string>([ private static readonly mimeTypes = new Map<FileType, string>([
// Some browsers (including firefox + IE) require right mime type // Some browsers (including firefox + IE) require right mime type
// otherwise they ignore extension and save the file as text. // otherwise they ignore extension and save the file as text.

View File

@@ -2,8 +2,11 @@ import { EventSource } from '../Events/EventSource';
export class AsyncLazy<T> { export class AsyncLazy<T> {
private valueCreated = new EventSource(); private valueCreated = new EventSource();
private isValueCreated = false; private isValueCreated = false;
private isCreatingValue = false; private isCreatingValue = false;
private value: T | undefined; private value: T | undefined;
constructor(private valueFactory: () => Promise<T>) {} constructor(private valueFactory: () => Promise<T>) {}
@@ -19,7 +22,7 @@ export class AsyncLazy<T> {
} }
// If value is being created, wait until the value is created and then return it. // If value is being created, wait until the value is created and then return it.
if (this.isCreatingValue) { if (this.isCreatingValue) {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve) => {
// Return/result when valueCreated event is triggered. // Return/result when valueCreated event is triggered.
this.valueCreated.on(() => resolve(this.value)); this.valueCreated.on(() => resolve(this.value));
}); });

View File

@@ -1,5 +1,8 @@
export type SchedulerType = (callback: (...args: any[]) => void, ms: number) => void; export type SchedulerCallbackType = (...args: unknown[]) => void;
export type SchedulerType = (callback: SchedulerCallbackType, ms: number) => void;
export function sleep(time: number, scheduler: SchedulerType = setTimeout) { export function sleep(time: number, scheduler: SchedulerType = setTimeout) {
return new Promise((resolve) => scheduler(() => resolve(undefined), time)); return new Promise((resolve) => {
scheduler(() => resolve(undefined), time);
});
} }

View File

@@ -7,14 +7,13 @@ import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
export class ApplicationBootstrapper implements IVueBootstrapper { export class ApplicationBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {
vue.config.productionTip = false; const bootstrappers = ApplicationBootstrapper.getAllBootstrappers();
const bootstrappers = this.getAllBootstrappers();
for (const bootstrapper of bootstrappers) { for (const bootstrapper of bootstrappers) {
bootstrapper.bootstrap(vue); bootstrapper.bootstrap(vue);
} }
} }
private getAllBootstrappers(): IVueBootstrapper[] { private static getAllBootstrappers(): IVueBootstrapper[] {
return [ return [
new IconBootstrapper(), new IconBootstrapper(),
new TreeBootstrapper(), new TreeBootstrapper(),

View File

@@ -1,4 +1,3 @@
import { IVueBootstrapper, VueConstructor } from './../IVueBootstrapper';
import { library } from '@fortawesome/fontawesome-svg-core'; import { library } from '@fortawesome/fontawesome-svg-core';
import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons';
/** BRAND ICONS (PREFIX: fab) */ /** BRAND ICONS (PREFIX: fab) */
@@ -6,8 +5,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
/** REGULAR ICONS (PREFIX: far) */ /** REGULAR ICONS (PREFIX: far) */
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons'; import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
/** SOLID ICONS (PREFIX: fas (default)) */ /** SOLID ICONS (PREFIX: fas (default)) */
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, import {
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH } from '@fortawesome/free-solid-svg-icons'; faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe,
faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH,
} from '@fortawesome/free-solid-svg-icons';
import { IVueBootstrapper, VueConstructor } from '../IVueBootstrapper';
export class IconBootstrapper implements IVueBootstrapper { export class IconBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {
@@ -21,11 +23,13 @@ export class IconBootstrapper implements IVueBootstrapper {
faFolderOpen, faFolderOpen,
faFolder, faFolder,
faTimes, faTimes,
faFileDownload, faSave, faFileDownload,
faSave,
faCopy, faCopy,
faPlay, faPlay,
faSearch, faSearch,
faBatteryFull, faBatteryHalf, faBatteryFull,
faBatteryHalf,
faInfoCircle, faInfoCircle,
faArrowsAltH, faArrowsAltH,
); );

View File

@@ -1,5 +1,5 @@
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
import VTooltip from 'v-tooltip'; import VTooltip from 'v-tooltip';
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
export class TooltipBootstrapper implements IVueBootstrapper { export class TooltipBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {

View File

@@ -1,5 +1,5 @@
import LiquorTree from 'liquor-tree'; import LiquorTree from 'liquor-tree';
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper'; import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
export class TreeBootstrapper implements IVueBootstrapper { export class TreeBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {

View File

@@ -1,5 +1,5 @@
import VModal from 'vue-js-modal'; import VModal from 'vue-js-modal';
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper'; import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
export class VModalBootstrapper implements IVueBootstrapper { export class VModalBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {

View File

@@ -1,7 +1,8 @@
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper'; import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
export class VueBootstrapper implements IVueBootstrapper { export class VueBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void { public bootstrap(vue: VueConstructor): void {
vue.config.productionTip = false; const { config } = vue;
config.productionTip = false;
} }
} }

View File

@@ -8,18 +8,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator'; import {
Component, Prop, Emit, Vue,
} from 'vue-property-decorator';
@Component @Component
export default class IconButton extends Vue { export default class IconButton extends Vue {
@Prop() public text!: number; @Prop() public text!: number;
@Prop() public iconPrefix!: string; @Prop() public iconPrefix!: string;
@Prop() public iconName!: string; @Prop() public iconName!: string;
@Emit('click') @Emit('click') public onClicked() { /* do nothing except firing event */ }
public onClicked() {
return;
}
} }
</script> </script>
@@ -63,5 +64,4 @@ export default class IconButton extends Vue {
line-height: 1.1; line-height: 1.1;
} }
} }
</style> </style>

View File

@@ -1,7 +1,8 @@
<template> <template>
<div class="instructions"> <div class="instructions">
<p> <p>
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS: Since you're using online version of {{ this.appName }}, you will need to do additional
steps after downloading the file to execute your script on macOS:
</p> </p>
<p> <p>
<ol> <ol>
@@ -10,7 +11,9 @@
<font-awesome-icon <font-awesome-icon
class="explanation" class="explanation"
:icon="['fas', 'info-circle']" :icon="['fas', 'info-circle']"
v-tooltip.top-center="'You should be prompted to save the script file now, otherwise try to download it again'" v-tooltip.top-center="
'You should be prompted to save the script file now'
+ ', otherwise try to download it again'"
/> />
</li> </li>
<li> <li>
@@ -18,7 +21,8 @@
<font-awesome-icon <font-awesome-icon
class="explanation" class="explanation"
:icon="['fas', 'info-circle']" :icon="['fas', 'info-circle']"
v-tooltip.top-center="'Type Terminal into Spotlight or open from the Applications -> Utilities folder'" v-tooltip.top-center="
'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
/> />
</li> </li>
<li> <li>
@@ -29,10 +33,11 @@
class="explanation" class="explanation"
:icon="['fas', 'info-circle']" :icon="['fas', 'info-circle']"
v-tooltip.top-center=" v-tooltip.top-center="
'Press on Enter/Return key after running the command.<br/>' + 'Press on Enter/Return key after running the command.<br/>'
'If the file is not downloaded on Downloads folder, change `Downloads` to path where the file is downloaded.<br/>' + + 'If the file is not downloaded on Downloads folder, change'
' `cd` will change the current folder.<br/>' + + '`Downloads` to path where the file is downloaded.<br/>'
' `~` is the user home directory.'" + ' `cd` will change the current folder.<br/>'
+ ' `~` is the user home directory.'"
/> />
</div> </div>
</li> </li>
@@ -74,16 +79,17 @@
</ol> </ol>
</p> </p>
<p> <p>
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps. Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts
directly to skip these steps.
</p> </p>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import Code from './Code.vue';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { ApplicationFactory } from '@/application/ApplicationFactory'; import { ApplicationFactory } from '@/application/ApplicationFactory';
import Code from './Code.vue';
@Component({ @Component({
components: { components: {
@@ -92,7 +98,9 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
}) })
export default class MacOsInstructions extends Vue { export default class MacOsInstructions extends Vue {
@Prop() public fileName: string; @Prop() public fileName: string;
public appName = ''; public appName = '';
public macOsDownloadUrl = ''; public macOsDownloadUrl = '';
public async created() { public async created() {

View File

@@ -29,8 +29,6 @@ import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog'; import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
import { Clipboard } from '@/infrastructure/Clipboard'; import { Clipboard } from '@/infrastructure/Clipboard';
import Dialog from '@/presentation/components/Shared/Dialog.vue'; import Dialog from '@/presentation/components/Shared/Dialog.vue';
import IconButton from './IconButton.vue';
import MacOsInstructions from './MacOsInstructions.vue';
import { Environment } from '@/application/Environment/Environment'; import { Environment } from '@/application/Environment/Environment';
import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
@@ -39,6 +37,8 @@ import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/infrastructure/CodeRunner'; import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext'; import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
import MacOsInstructions from './MacOsInstructions.vue';
import IconButton from './IconButton.vue';
@Component({ @Component({
components: { components: {
@@ -49,39 +49,47 @@ import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationC
}) })
export default class TheCodeButtons extends StatefulVue { export default class TheCodeButtons extends StatefulVue {
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop; public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
public canRun = false; public canRun = false;
public hasCode = false; public hasCode = false;
public isMacOsCollection = false; public isMacOsCollection = false;
public fileName = ''; public fileName = '';
public async copyCode() { public async copyCode() {
const code = await this.getCurrentCode(); const code = await this.getCurrentCode();
Clipboard.copyText(code.current); Clipboard.copyText(code.current);
} }
public async saveCode() { public async saveCode() {
const context = await this.getCurrentContext(); const context = await this.getCurrentContext();
saveCode(this.fileName, context.state); saveCode(this.fileName, context.state);
if (this.isMacOsCollection) { if (this.isMacOsCollection) {
(this.$refs.instructionsDialog as any).show(); (this.$refs.instructionsDialog as Dialog).show();
} }
} }
public async executeCode() { public async executeCode() {
const context = await this.getCurrentContext(); const context = await this.getCurrentContext();
await executeCode(context); await executeCode(context);
} }
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os; const isNewOs = (test: OperatingSystem) => newState.collection.os === test;
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS; this.canRun = this.isDesktopVersion && isNewOs(Environment.CurrentEnvironment.os);
this.isMacOsCollection = isNewOs(OperatingSystem.macOS);
this.fileName = buildFileName(newState.collection.scripting); this.fileName = buildFileName(newState.collection.scripting);
this.react(newState.code); this.react(newState.code);
} }
private async getCurrentCode(): Promise<IApplicationCode> { private async getCurrentCode(): Promise<IApplicationCode> {
const context = await this.getCurrentContext(); const context = await this.getCurrentContext();
const code = context.state.code; const { code } = context.state;
return code; return code;
} }
private async react(code: IApplicationCode) { private async react(code: IApplicationCode) {
this.hasCode = code.current && code.current.length > 0; this.hasCode = code.current && code.current.length > 0;
this.events.unsubscribeAll(); this.events.unsubscribeAll();
@@ -118,9 +126,9 @@ function buildFileName(scripting: IScriptingDefinition) {
async function executeCode(context: IReadOnlyApplicationContext) { async function executeCode(context: IReadOnlyApplicationContext) {
const runner = new CodeRunner(); const runner = new CodeRunner();
await runner.runCode( await runner.runCode(
/*code*/ context.state.code.current, /* code: */ context.state.code.current,
/*appName*/ context.app.info.name, /* appName: */ context.app.info.name,
/*fileExtension*/ context.state.collection.scripting.fileExtension, /* fileExtension: */ context.state.collection.scripting.fileExtension,
); );
} }

View File

@@ -31,6 +31,7 @@ export default class TheCodeArea extends StatefulVue {
public readonly editorId = 'codeEditor'; public readonly editorId = 'codeEditor';
private editor!: ace.Ace.Editor; private editor!: ace.Ace.Editor;
private currentMarkerId?: number; private currentMarkerId?: number;
@Prop() private theme!: string; @Prop() private theme!: string;
@@ -38,6 +39,7 @@ export default class TheCodeArea extends StatefulVue {
public destroyed() { public destroyed() {
this.destroyEditor(); this.destroyEditor();
} }
public sizeChanged() { public sizeChanged() {
if (this.editor) { if (this.editor) {
this.editor.resize(); this.editor.resize();
@@ -46,9 +48,14 @@ export default class TheCodeArea extends StatefulVue {
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void { protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
this.destroyEditor(); this.destroyEditor();
this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language); this.editor = initializeEditor(
this.theme,
this.editorId,
newState.collection.scripting.language,
);
const appCode = newState.code; const appCode = newState.code;
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1); const innerCode = appCode.current || getDefaultCode(newState.collection.scripting.language);
this.editor.setValue(innerCode, 1);
this.events.unsubscribeAll(); this.events.unsubscribeAll();
this.events.register(appCode.changed.on((code) => this.updateCode(code))); this.events.register(appCode.changed.on((code) => this.updateCode(code)));
} }
@@ -68,6 +75,7 @@ export default class TheCodeArea extends StatefulVue {
this.reactToChanges(event, event.changedScripts); this.reactToChanges(event, event.changedScripts);
} }
} }
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) { private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
const positions = scripts const positions = scripts
.map((script) => event.getScriptPositionInCode(script)); .map((script) => event.getScriptPositionInCode(script));
@@ -80,16 +88,21 @@ export default class TheCodeArea extends StatefulVue {
this.scrollToLine(end + 2); this.scrollToLine(end + 2);
this.highlight(start, end); this.highlight(start, end);
} }
private highlight(startRow: number, endRow: number) { private highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range; const AceRange = ace.require('ace/range').Range;
this.currentMarkerId = this.editor.session.addMarker( this.currentMarkerId = this.editor.session.addMarker(
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine', new AceRange(startRow, 0, endRow, 0),
'code-area__highlight',
'fullLine',
); );
} }
private scrollToLine(row: number) { private scrollToLine(row: number) {
const column = this.editor.session.getLine(row).length; const column = this.editor.session.getLine(row).length;
this.editor.gotoLine(row, column, true); this.editor.gotoLine(row, column, true);
} }
private removeCurrentHighlighting() { private removeCurrentHighlighting() {
if (!this.currentMarkerId) { if (!this.currentMarkerId) {
return; return;
@@ -97,6 +110,7 @@ export default class TheCodeArea extends StatefulVue {
this.editor.session.removeMarker(this.currentMarkerId); this.editor.session.removeMarker(this.currentMarkerId);
this.currentMarkerId = undefined; this.currentMarkerId = undefined;
} }
private destroyEditor() { private destroyEditor() {
if (this.editor) { if (this.editor) {
this.editor.destroy(); this.editor.destroy();
@@ -105,7 +119,11 @@ export default class TheCodeArea extends StatefulVue {
} }
} }
function initializeEditor(theme: string, editorId: string, language: ScriptingLanguage): ace.Ace.Editor { function initializeEditor(
theme: string,
editorId: string,
language: ScriptingLanguage,
): ace.Ace.Editor {
theme = theme || 'github'; theme = theme || 'github';
const editor = ace.edit(editorId); const editor = ace.edit(editorId);
const lang = getLanguage(language); const lang = getLanguage(language);

View File

@@ -10,8 +10,8 @@ import 'ace-builds/src-noconflict/theme-xcode';
import 'ace-builds/src-noconflict/mode-batchfile'; import 'ace-builds/src-noconflict/mode-batchfile';
import 'ace-builds/src-noconflict/mode-sh'; import 'ace-builds/src-noconflict/mode-sh';
ace.config.setModuleUrl('ace/mode/html_worker', new URL('ace-builds/src-noconflict/worker-html.js', import.meta.url).toString()) ace.config.setModuleUrl('ace/mode/html_worker', new URL('ace-builds/src-noconflict/worker-html.js', import.meta.url).toString());
ace.config.setModuleUrl('ace/mode/javascript_worker', new URL('ace-builds/src-noconflict/worker-javascript.js', import.meta.url).toString()) ace.config.setModuleUrl('ace/mode/javascript_worker', new URL('ace-builds/src-noconflict/worker-javascript.js', import.meta.url).toString());
ace.config.setModuleUrl('ace/mode/json_worker', new URL('ace-builds/src-noconflict/worker-json.js', import.meta.url).toString()) ace.config.setModuleUrl('ace/mode/json_worker', new URL('ace-builds/src-noconflict/worker-json.js', import.meta.url).toString());
export default ace; export default ace;

View File

@@ -8,7 +8,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator'; import {
Component, Prop, Emit, Vue,
} from 'vue-property-decorator';
import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective'; import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
@Component({ @Component({
@@ -16,8 +18,10 @@ import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonC
}) })
export default class MenuOptionListItem extends Vue { export default class MenuOptionListItem extends Vue {
@Prop() public enabled: boolean; @Prop() public enabled: boolean;
@Prop() public label: string; @Prop() public label: string;
@Emit('click') public onClicked() { return; }
@Emit('click') public onClicked() { /* do nothing except firing event */ }
} }
</script> </script>

View File

@@ -16,6 +16,7 @@ export class SelectionTypeHandler {
constructor(private readonly state: ICategoryCollectionState) { constructor(private readonly state: ICategoryCollectionState) {
if (!state) { throw new Error('undefined state'); } if (!state) { throw new Error('undefined state'); }
} }
public selectType(type: SelectionType) { public selectType(type: SelectionType) {
if (type === SelectionType.Custom) { if (type === SelectionType.Custom) {
throw new Error('cannot select custom type'); throw new Error('cannot select custom type');
@@ -23,6 +24,7 @@ export class SelectionTypeHandler {
const selector = selectors.get(type); const selector = selectors.get(type);
selector.select(this.state); selector.select(this.state);
} }
public getCurrentSelectionType(): SelectionType { public getCurrentSelectionType(): SelectionType {
for (const [type, selector] of Array.from(selectors.entries())) { for (const [type, selector] of Array.from(selectors.entries())) {
if (selector.isSelected(this.state)) { if (selector.isSelected(this.state)) {
@@ -40,18 +42,14 @@ interface ISingleTypeHandler {
const selectors = new Map<SelectionType, ISingleTypeHandler>([ const selectors = new Map<SelectionType, ISingleTypeHandler>([
[SelectionType.None, { [SelectionType.None, {
select: (state) => select: (state) => state.selection.deselectAll(),
state.selection.deselectAll(), isSelected: (state) => state.selection.selectedScripts.length === 0,
isSelected: (state) =>
state.selection.selectedScripts.length === 0,
}], }],
[SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)],
[SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)],
[SelectionType.All, { [SelectionType.All, {
select: (state) => select: (state) => state.selection.selectAll(),
state.selection.selectAll(), isSelected: (state) => state.selection.selectedScripts.length === state.collection.totalScripts,
isSelected: (state) =>
state.selection.selectedScripts.length === state.collection.totalScripts,
}], }],
]); ]);
@@ -62,9 +60,12 @@ function getRecommendationLevelSelector(level: RecommendationLevel): ISingleType
}; };
} }
function hasAllSelectedLevelOf(level: RecommendationLevel, state: IReadOnlyCategoryCollectionState) { function hasAllSelectedLevelOf(
level: RecommendationLevel,
state: IReadOnlyCategoryCollectionState,
) {
const scripts = state.collection.getScriptsByLevel(level); const scripts = state.collection.getScriptsByLevel(level);
const selectedScripts = state.selection.selectedScripts; const { selectedScripts } = state.selection;
return areAllSelected(scripts, selectedScripts); return areAllSelected(scripts, selectedScripts);
} }
@@ -75,12 +76,14 @@ function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState)
function areAllSelected( function areAllSelected(
expectedScripts: ReadonlyArray<IScript>, expectedScripts: ReadonlyArray<IScript>,
selection: ReadonlyArray<SelectedScript>): boolean { selection: ReadonlyArray<SelectedScript>,
selection = selection.filter((selected) => !selected.revert); ): boolean {
if (expectedScripts.length < selection.length) { const selectedScriptIds = selection
.filter((selected) => !selected.revert)
.map((script) => script.id);
if (expectedScripts.length < selectedScriptIds.length) {
return false; return false;
} }
const selectedScriptIds = selection.map((script) => script.id);
const expectedScriptIds = expectedScripts.map((script) => script.id); const expectedScriptIds = expectedScripts.map((script) => script.id);
return scrambledEqual(selectedScriptIds, expectedScriptIds); return scrambledEqual(selectedScriptIds, expectedScriptIds);
} }

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