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:
163
.eslintrc.js
163
.eslintrc.js
@@ -1,3 +1,5 @@
|
||||
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
@@ -22,91 +24,10 @@ module.exports = {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
|
||||
'linebreak-style': 'off',
|
||||
'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',
|
||||
...getOwnRules(),
|
||||
...getTurnedOffBrokenRules(),
|
||||
...getOpinionatedRuleOverrides(),
|
||||
...getTodoRules(),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -118,5 +39,77 @@ module.exports = {
|
||||
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'),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||
"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",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"postuninstall": "electron-builder install-app-deps",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,13 +8,16 @@ const ApplicationGetter: ApplicationGetter = parseApplication;
|
||||
|
||||
export class ApplicationFactory implements IApplicationFactory {
|
||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||
|
||||
private readonly getter: AsyncLazy<IApplication>;
|
||||
|
||||
protected constructor(costlyGetter: ApplicationGetter) {
|
||||
if (!costlyGetter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||
}
|
||||
|
||||
public getApp(): Promise<IApplication> {
|
||||
return this.getter.getValue();
|
||||
}
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||
export type EnumType = number | string;
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||
= { [key in T]: TEnumValue };
|
||||
|
||||
export interface IEnumParser<TEnum> {
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
}
|
||||
|
||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: string,
|
||||
enumName: string,
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): TEnumValue {
|
||||
if (!value) {
|
||||
throw new Error(`undefined ${enumName}`);
|
||||
}
|
||||
@@ -29,22 +34,26 @@ function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||
}
|
||||
|
||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
||||
export function getEnumNames
|
||||
<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
}
|
||||
|
||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
): TEnumValue[] {
|
||||
return getEnumNames(enumVariable)
|
||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||
}
|
||||
|
||||
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: TEnumValue,
|
||||
enumVariable: EnumVariable<T, TEnumValue>) {
|
||||
enumVariable: EnumVariable<T, TEnumValue>,
|
||||
) {
|
||||
if (value === undefined) {
|
||||
throw new Error('undefined enum value');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
@@ -27,5 +27,4 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
||||
}
|
||||
this.getters.set(language, getter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||
|
||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||
|
||||
export class ApplicationContext implements IApplicationContext {
|
||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||
|
||||
public collection: ICategoryCollection;
|
||||
|
||||
public currentOs: OperatingSystem;
|
||||
|
||||
public get state(): ICategoryCollectionState {
|
||||
@@ -19,9 +21,11 @@ export class ApplicationContext implements IApplicationContext {
|
||||
}
|
||||
|
||||
private readonly states: StateMachine;
|
||||
|
||||
public constructor(
|
||||
public readonly app: IApplication,
|
||||
initialContext: OperatingSystem) {
|
||||
initialContext: OperatingSystem,
|
||||
) {
|
||||
validateApp(app);
|
||||
assertInRange(initialContext, OperatingSystem);
|
||||
this.states = initializeStates(app);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
|
||||
export async function buildContext(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
||||
environment = Environment.CurrentEnvironment,
|
||||
): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('undefined factory'); }
|
||||
if (!environment) { throw new Error('undefined environment'); }
|
||||
const app = await factory.getApp();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
|
||||
export interface IReadOnlyApplicationContext {
|
||||
readonly app: IApplication;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { UserFilter } from './Filter/UserFilter';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
@@ -5,13 +7,14 @@ import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
public readonly code: IApplicationCode;
|
||||
|
||||
public readonly selection: IUserSelection;
|
||||
|
||||
public readonly filter: IUserFilter;
|
||||
|
||||
public constructor(readonly collection: ICategoryCollection) {
|
||||
|
||||
@@ -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 { CodePosition } from './Position/CodePosition';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||
|
||||
public current: string;
|
||||
|
||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||
@@ -18,7 +19,8 @@ export class ApplicationCode implements IApplicationCode {
|
||||
constructor(
|
||||
userSelection: IReadOnlyUserSelection,
|
||||
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 (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
public readonly code: string;
|
||||
|
||||
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||
|
||||
private readonly scripts: Map<IScript, ICodePosition>;
|
||||
@@ -14,7 +17,8 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
constructor(
|
||||
code: string,
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
scripts: Map<SelectedScript, ICodePosition>) {
|
||||
scripts: Map<SelectedScript, ICodePosition>,
|
||||
) {
|
||||
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||
this.code = code;
|
||||
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;
|
||||
for (const position of positions) {
|
||||
if (position.endLine > totalLines) {
|
||||
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||
`(total code lines: ${totalLines}`);
|
||||
throw new Error(
|
||||
`script end line (${position.endLine}) is out of range.`
|
||||
+ `(total code lines: ${totalLines}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getChangedScripts(
|
||||
oldScripts: ReadonlyArray<SelectedScript>,
|
||||
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||
newScripts: ReadonlyArray<SelectedScript>,
|
||||
): ReadonlyArray<IScript> {
|
||||
return newScripts
|
||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||
&& oldScript.revert !== newScript.revert))
|
||||
@@ -57,7 +64,8 @@ function getChangedScripts(
|
||||
|
||||
function selectIfNotExists(
|
||||
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||
test: ReadonlyArray<SelectedScript>) {
|
||||
test: ReadonlyArray<SelectedScript>,
|
||||
) {
|
||||
return selectableContainer
|
||||
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||
.map((selection) => selection.script);
|
||||
|
||||
@@ -24,7 +24,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
}
|
||||
|
||||
public appendTrailingHyphensCommentLine(
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||
): CodeBuilder {
|
||||
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
|
||||
}
|
||||
|
||||
@@ -45,7 +46,8 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
|
||||
public appendCommentLineWithHyphensAround(
|
||||
sectionName: string,
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
|
||||
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
|
||||
): CodeBuilder {
|
||||
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
|
||||
if (sectionName.length >= totalRepeatHyphens) {
|
||||
return this.appendCommentLine(sectionName);
|
||||
@@ -63,5 +65,6 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
||||
}
|
||||
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { BatchBuilder } from './Languages/BatchBuilder';
|
||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
|
||||
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
|
||||
export class CodeBuilderFactory
|
||||
extends ScriptingLanguageFactory<ICodeBuilder>
|
||||
implements ICodeBuilderFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
|
||||
}
|
||||
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
|
||||
@@ -4,6 +4,7 @@ export class BatchBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '::';
|
||||
}
|
||||
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export class ShellBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '#';
|
||||
}
|
||||
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||
@@ -12,9 +12,11 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||
|
||||
}
|
||||
|
||||
public buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
||||
scriptingDefinition: IScriptingDefinition,
|
||||
): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
@@ -52,9 +54,11 @@ function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
||||
builder = appendCode(selection, builder);
|
||||
builder: ICodeBuilder,
|
||||
): Map<SelectedScript, ICodePosition> {
|
||||
// Start from next line because first line will be empty to separate scripts
|
||||
const startPosition = builder.currentLine + 1;
|
||||
appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
const position = new CodePosition(startPosition, endPosition);
|
||||
@@ -63,8 +67,9 @@ function appendSelection(
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||
const { script } = selection;
|
||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
||||
return builder
|
||||
.appendLine()
|
||||
.appendFunction(name, scriptCode);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
|
||||
export interface IApplicationCode {
|
||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||
|
||||
@@ -7,7 +7,8 @@ export class CodePosition implements ICodePosition {
|
||||
|
||||
constructor(
|
||||
public readonly startLine: number,
|
||||
public readonly endLine: number) {
|
||||
public readonly endLine: number,
|
||||
) {
|
||||
if (startLine < 0) {
|
||||
throw new Error('Code cannot start in a negative line');
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
|
||||
export class FilterResult implements IFilterResult {
|
||||
constructor(
|
||||
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||
public readonly query: string) {
|
||||
public readonly query: string,
|
||||
) {
|
||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
||||
}
|
||||
|
||||
public hasAnyMatches(): boolean {
|
||||
return this.scriptMatches.length > 0
|
||||
|| this.categoryMatches.length > 0;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new EventSource<IFilterResult>();
|
||||
|
||||
public readonly filterRemoved = new EventSource<void>();
|
||||
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
|
||||
constructor(private collection: ICategoryCollection) {
|
||||
@@ -20,9 +22,11 @@ export class UserFilter implements IUserFilter {
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.collection.getAllScripts().filter(
|
||||
(script) => isScriptAMatch(script, filterLowercase));
|
||||
(script) => isScriptAMatch(script, filterLowercase),
|
||||
);
|
||||
const filteredCategories = this.collection.getAllCategories().filter(
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase),
|
||||
);
|
||||
const matches = new FilterResult(
|
||||
filteredScripts,
|
||||
filteredCategories,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IReadOnlyCategoryCollectionState {
|
||||
readonly code: IApplicationCode;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export interface IReadOnlyUserSelection {
|
||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
constructor(
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
for (const script of selectedScripts) {
|
||||
@@ -30,7 +32,9 @@ export class UserSelection implements IUserSelection {
|
||||
if (this.selectedScripts.length < scripts.length) {
|
||||
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 {
|
||||
@@ -53,11 +57,11 @@ export class UserSelection implements IUserSelection {
|
||||
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 scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||
.filter((script) =>
|
||||
!this.scripts.exists(script.id)
|
||||
.filter(
|
||||
(script) => !this.scripts.exists(script.id)
|
||||
|| this.scripts.getById(script.id).revert !== revert,
|
||||
);
|
||||
if (!scriptsToAddOrUpdate.length) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||
private readonly detectors = BrowserDetectors;
|
||||
|
||||
public detect(userAgent: string): OperatingSystem | undefined {
|
||||
if (!userAgent) {
|
||||
return undefined;
|
||||
@@ -19,35 +20,37 @@ export class BrowserOsDetector implements IBrowserOsDetector {
|
||||
}
|
||||
|
||||
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||
const BrowserDetectors =
|
||||
[
|
||||
define(OperatingSystem.KaiOS, (b) =>
|
||||
b.mustInclude('KAIOS')),
|
||||
define(OperatingSystem.ChromeOS, (b) =>
|
||||
b.mustInclude('CrOS')),
|
||||
define(OperatingSystem.BlackBerryOS, (b) =>
|
||||
b.mustInclude('BlackBerry')),
|
||||
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
||||
b.mustInclude('RIM Tablet OS')),
|
||||
define(OperatingSystem.BlackBerry, (b) =>
|
||||
b.mustInclude('BB10')),
|
||||
define(OperatingSystem.Android, (b) =>
|
||||
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.Android, (b) =>
|
||||
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.iOS, (b) =>
|
||||
b.mustInclude('like Mac OS X')),
|
||||
define(OperatingSystem.Linux, (b) =>
|
||||
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||
define(OperatingSystem.Windows, (b) =>
|
||||
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.WindowsPhone, (b) =>
|
||||
b.mustInclude('Windows Phone')),
|
||||
define(OperatingSystem.macOS, (b) =>
|
||||
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||
const BrowserDetectors = [
|
||||
define(OperatingSystem.KaiOS, (b) => b
|
||||
.mustInclude('KAIOS')),
|
||||
define(OperatingSystem.ChromeOS, (b) => b
|
||||
.mustInclude('CrOS')),
|
||||
define(OperatingSystem.BlackBerryOS, (b) => b
|
||||
.mustInclude('BlackBerry')),
|
||||
define(OperatingSystem.BlackBerryTabletOS, (b) => b
|
||||
.mustInclude('RIM Tablet OS')),
|
||||
define(OperatingSystem.BlackBerry, (b) => b
|
||||
.mustInclude('BB10')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.Android, (b) => b
|
||||
.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.iOS, (b) => b
|
||||
.mustInclude('like Mac OS X')),
|
||||
define(OperatingSystem.Linux, (b) => b
|
||||
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||
define(OperatingSystem.Windows, (b) => b
|
||||
.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||
define(OperatingSystem.WindowsPhone, (b) => b
|
||||
.mustInclude('Windows Phone')),
|
||||
define(OperatingSystem.macOS, (b) => b
|
||||
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||
];
|
||||
|
||||
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
||||
function define(
|
||||
os: OperatingSystem,
|
||||
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
|
||||
): IBrowserOsDetector {
|
||||
const builder = new DetectorBuilder(os);
|
||||
applyRules(builder);
|
||||
return builder.build();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||
|
||||
export class DetectorBuilder {
|
||||
private readonly existingPartsInUserAgent = new Array<string>();
|
||||
|
||||
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||
|
||||
constructor(private readonly os: OperatingSystem) { }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||
import { IEnvironment } from './IEnvironment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
interface IEnvironmentVariables {
|
||||
export interface IEnvironmentVariables {
|
||||
readonly window: Window & typeof globalThis;
|
||||
readonly process: NodeJS.Process;
|
||||
readonly navigator: Navigator;
|
||||
@@ -15,11 +15,15 @@ export class Environment implements IEnvironment {
|
||||
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
|
||||
navigator,
|
||||
});
|
||||
|
||||
public readonly isDesktop: boolean;
|
||||
|
||||
public readonly os: OperatingSystem;
|
||||
|
||||
protected constructor(
|
||||
variables: IEnvironmentVariables,
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
||||
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
|
||||
) {
|
||||
if (!variables) {
|
||||
throw new Error('variables is null or empty');
|
||||
}
|
||||
@@ -49,15 +53,17 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||
|
||||
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||
// https://nodejs.org/api/process.html#process_process_platform
|
||||
if (processPlatform === 'darwin') {
|
||||
switch (processPlatform) {
|
||||
case 'darwin':
|
||||
return OperatingSystem.macOS;
|
||||
} else if (processPlatform === 'win32') {
|
||||
case 'win32':
|
||||
return OperatingSystem.Windows;
|
||||
} else if (processPlatform === 'linux') {
|
||||
case 'linux':
|
||||
return OperatingSystem.Linux;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||
// More: https://github.com/electron/electron/issues/2288
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
collectionsData = PreParsedCollections): IApplication {
|
||||
collectionsData = PreParsedCollections,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
@@ -22,11 +23,13 @@ export function parseApplication(
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||
|
||||
const CategoryCollectionParser: CategoryCollectionParserType
|
||||
= (file, info) => parseCategoryCollection(file, info);
|
||||
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
||||
return parseCategoryCollection(file, info);
|
||||
};
|
||||
|
||||
const PreParsedCollections: readonly CollectionData []
|
||||
= [ WindowsData, MacOsData ];
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
WindowsData, MacOsData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections.length) {
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Category } from '@/domain/Category';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
info: IProjectInformation,
|
||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||
osParser = createEnumParser(OperatingSystem),
|
||||
): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = new ScriptingDefinitionParser()
|
||||
.parse(content.scripting, info);
|
||||
@@ -26,7 +27,8 @@ export function parseCategoryCollection(
|
||||
const collection = new CategoryCollection(
|
||||
os,
|
||||
categories,
|
||||
scripting);
|
||||
scripting,
|
||||
);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
|
||||
let categoryIdCounter: number = 0;
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
interface ICategoryChildren {
|
||||
subCategories: Category[];
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
||||
export function parseCategory(
|
||||
category: CategoryData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
): Category {
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
ensureValid(category);
|
||||
const children: ICategoryChildren = {
|
||||
@@ -23,11 +28,11 @@ export function parseCategory(category: CategoryData, context: ICategoryCollecti
|
||||
parseCategoryChild(data, children, category, context);
|
||||
}
|
||||
return new Category(
|
||||
/*id*/ categoryIdCounter++,
|
||||
/*name*/ category.category,
|
||||
/*docs*/ parseDocUrls(category),
|
||||
/*categories*/ children.subCategories,
|
||||
/*scripts*/ children.subScripts,
|
||||
/* id: */ categoryIdCounter++,
|
||||
/* name: */ category.category,
|
||||
/* docs: */ parseDocUrls(category),
|
||||
/* categories: */ children.subCategories,
|
||||
/* scripts: */ children.subScripts,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,7 +52,8 @@ function parseCategoryChild(
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext) {
|
||||
context: ICategoryCollectionParseContext,
|
||||
) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
@@ -61,11 +67,20 @@ function parseCategoryChild(
|
||||
}
|
||||
}
|
||||
|
||||
function isScript(data: any): boolean {
|
||||
return (data.code && data.code.length > 0)
|
||||
|| data.call;
|
||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||
const holder = (data as InstructionHolder);
|
||||
return hasCode(holder) || hasCall(holder);
|
||||
}
|
||||
|
||||
function isCategory(data: any): boolean {
|
||||
return data.category && data.category.length > 0;
|
||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||
const { category } = data as CategoryData;
|
||||
return category && category.length > 0;
|
||||
}
|
||||
|
||||
function hasCode(holder: InstructionHolder): boolean {
|
||||
return holder.code && holder.code.length > 0;
|
||||
}
|
||||
|
||||
function hasCall(holder: InstructionHolder) {
|
||||
return holder.call !== undefined;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
||||
if (!documentable) {
|
||||
throw new Error('documentable is null or undefined');
|
||||
}
|
||||
const docs = documentable.docs;
|
||||
const { docs } = documentable;
|
||||
if (!docs || !docs.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -13,7 +13,10 @@ export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<stri
|
||||
return result.getAll();
|
||||
}
|
||||
|
||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||
function addDocs(
|
||||
docs: DocumentationUrlsData,
|
||||
urls: DocumentationUrlContainer,
|
||||
): DocumentationUrlContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
} else if (typeof docs === 'string') {
|
||||
@@ -32,7 +35,7 @@ class DocumentationUrlContainer {
|
||||
this.urls.push(url);
|
||||
}
|
||||
|
||||
public addUrls(urls: readonly any[]) {
|
||||
public addUrls(urls: readonly string[]) {
|
||||
for (const url of urls) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||
@@ -53,8 +56,8 @@ function validateUrl(docUrl: string): void {
|
||||
if (docUrl.includes('\n')) {
|
||||
throw new Error('Documentation url cannot be multi-lined.');
|
||||
}
|
||||
const res = docUrl.match(
|
||||
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
|
||||
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
|
||||
const res = docUrl.match(validUrlRegex);
|
||||
if (res == null) {
|
||||
throw new Error(`Invalid documentation url: ${docUrl}`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
environment: NodeJS.ProcessEnv,
|
||||
): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
@@ -9,12 +9,14 @@ import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||
|
||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||
public readonly compiler: IScriptCompiler;
|
||||
|
||||
public readonly syntax: ILanguageSyntax;
|
||||
|
||||
constructor(
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||
) {
|
||||
if (!scripting) { throw new Error('undefined scripting'); }
|
||||
this.syntax = syntaxFactory.create(scripting.language);
|
||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpression } from './IExpression';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { IExpression } from './IExpression';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||
export class Expression implements IExpression {
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
||||
public readonly parameters
|
||||
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||
) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
@@ -20,14 +21,15 @@ export class Expression implements IExpression {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
}
|
||||
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('undefined context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(context);
|
||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(filteredContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,20 +45,19 @@ function validateThatAllRequiredParametersAreSatisfied(
|
||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||
if (missingParameterNames.length) {
|
||||
throw new Error(
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnusedArguments(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameter of parameters.all) {
|
||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
||||
continue; // Optional parameter is not necessarily provided
|
||||
}
|
||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
||||
specificCallArgs.addArgument(arg);
|
||||
}
|
||||
parameters.all
|
||||
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||
return specificCallArgs;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export interface IExpressionEvaluationContext {
|
||||
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||
constructor(
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||
) {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export class ExpressionPosition {
|
||||
constructor(
|
||||
public readonly start: number,
|
||||
public readonly end: number) {
|
||||
public readonly end: number,
|
||||
) {
|
||||
if (start === end) {
|
||||
throw new Error(`no length (start = end = ${start})`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export interface IExpression {
|
||||
|
||||
@@ -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 { IExpression } from './Expression/IExpression';
|
||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
|
||||
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public constructor(
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||
) { }
|
||||
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): string {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
@@ -29,7 +31,8 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
function compileExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext) {
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = expressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
@@ -51,7 +54,8 @@ function compileExpressions(
|
||||
}
|
||||
|
||||
function extractRequiredParameterNames(
|
||||
expressions: readonly IExpression[]): string[] {
|
||||
expressions: readonly IExpression[],
|
||||
): string[] {
|
||||
const usedParameterNames = expressions
|
||||
.map((e) => e.parameters.all
|
||||
.filter((p) => !p.isOptional)
|
||||
@@ -64,7 +68,8 @@ function extractRequiredParameterNames(
|
||||
|
||||
function ensureParamsUsedInCodeHasArgsProvided(
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
|
||||
const Parsers = [
|
||||
new ParameterSubstitutionParser(),
|
||||
@@ -14,6 +14,7 @@ export class CompositeExpressionParser implements IExpressionParser {
|
||||
throw new Error('undefined leaf');
|
||||
}
|
||||
}
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
const expressions = new Array<IExpression>();
|
||||
for (const parser of this.leafs) {
|
||||
|
||||
@@ -52,6 +52,7 @@ export class ExpressionRegexBuilder {
|
||||
return this
|
||||
.addRawRegex('\\s*');
|
||||
}
|
||||
|
||||
private addRawRegex(regex: string) {
|
||||
this.parts.push(regex);
|
||||
return this;
|
||||
|
||||
@@ -42,7 +42,8 @@ export interface IPrimitiveExpression {
|
||||
}
|
||||
|
||||
function getParameters(
|
||||
expression: IPrimitiveExpression): FunctionParameterCollection {
|
||||
expression: IPrimitiveExpression,
|
||||
): FunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
for (const parameter of expression.parameters || []) {
|
||||
parameters.addParameter(parameter);
|
||||
|
||||
@@ -2,8 +2,10 @@ import { IPipe } from '../IPipe';
|
||||
|
||||
export class EscapeDoubleQuotes implements IPipe {
|
||||
public readonly name: string = 'escapeDoubleQuotes';
|
||||
|
||||
public apply(raw: string): string {
|
||||
return raw?.replaceAll('"', '"^""');
|
||||
/* eslint-disable max-len */
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
@@ -11,17 +13,18 @@ export class EscapeDoubleQuotes implements IPipe {
|
||||
Breaks, because it is fundamentally unsupported
|
||||
""""
|
||||
Does not work with consecutive double quotes.
|
||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
||||
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||
\"
|
||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
|
||||
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
|
||||
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
|
||||
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||
\""
|
||||
Normalizes interior whitespace
|
||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
||||
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@ import { IPipe } from '../IPipe';
|
||||
|
||||
export class InlinePowerShell implements IPipe {
|
||||
public readonly name: string = 'inlinePowerShell';
|
||||
|
||||
public apply(code: string): string {
|
||||
if (!code || !hasLines(code)) {
|
||||
return code;
|
||||
}
|
||||
code = inlineComments(code);
|
||||
code = mergeLinesWithBacktick(code);
|
||||
code = mergeHereStrings(code);
|
||||
const lines = getLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
return lines
|
||||
.join('; ');
|
||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||
inlineComments,
|
||||
mergeLinesWithBacktick,
|
||||
mergeHereStrings,
|
||||
mergeNewLines,
|
||||
]).reduce((a, b) => (data) => b(a(data)));
|
||||
const newCode = processor(code);
|
||||
return newCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,15 +113,18 @@ interface IInlinedHereString {
|
||||
readonly escapedQuotes: string;
|
||||
readonly separator: string;
|
||||
}
|
||||
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||
/*
|
||||
We handle @' and @" differently.
|
||||
Single quotes are interpreted literally and doubles are expandable.
|
||||
*/
|
||||
const expandableNewLine = '`r`n';
|
||||
switch (quotes) {
|
||||
case '\'':
|
||||
return {
|
||||
quotesAround: '\'',
|
||||
escapedQuotes: '\'\'',
|
||||
separator: `\'+"${expandableNewLine}"+\'`,
|
||||
separator: `'+"${expandableNewLine}"+'`,
|
||||
};
|
||||
case '"':
|
||||
return {
|
||||
@@ -153,3 +157,10 @@ function mergeLinesWithBacktick(code: string) {
|
||||
*/
|
||||
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('; ');
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface IPipeFactory {
|
||||
|
||||
export class PipeFactory implements IPipeFactory {
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('undefined pipe in list');
|
||||
@@ -21,6 +22,7 @@ export class PipeFactory implements IPipeFactory {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
}
|
||||
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
@@ -28,6 +30,7 @@ export class PipeFactory implements IPipeFactory {
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
}
|
||||
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
validatePipeName(pipe.name);
|
||||
if (this.pipes.has(pipe.name)) {
|
||||
|
||||
@@ -3,14 +3,16 @@ import { IPipelineCompiler } from './IPipelineCompiler';
|
||||
|
||||
export class PipelineCompiler implements IPipelineCompiler {
|
||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||
|
||||
public compile(value: string, pipeline: string): string {
|
||||
ensureValidArguments(value, pipeline);
|
||||
const pipeNames = extractPipeNames(pipeline);
|
||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||
let valueInCompilation = value;
|
||||
for (const pipe of pipes) {
|
||||
value = pipe.apply(value);
|
||||
valueInCompilation = pipe.apply(valueInCompilation);
|
||||
}
|
||||
return value;
|
||||
return valueInCompilation;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class ParameterSubstitutionParser extends RegexParser {
|
||||
@@ -17,7 +17,7 @@ export class ParameterSubstitutionParser extends RegexParser {
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, false)],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
||||
const { argumentValue } = context.args.getArgument(parameterName);
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class WithParser extends RegexParser {
|
||||
@@ -25,8 +25,8 @@ export class WithParser extends RegexParser {
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, true)],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
||||
context.args.getArgument(parameterName).argumentValue
|
||||
const argumentValue = context.args.hasArgument(parameterName)
|
||||
? context.args.getArgument(parameterName).argumentValue
|
||||
: undefined;
|
||||
if (!argumentValue) {
|
||||
return '';
|
||||
@@ -51,7 +51,8 @@ const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||
.buildRegExp();
|
||||
|
||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||
// but instead letting the pipeline compiler to fail on those.
|
||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||
return replacer(match1);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||
|
||||
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||
constructor(
|
||||
public readonly parameterName: string,
|
||||
public readonly argumentValue: string) {
|
||||
public readonly argumentValue: string,
|
||||
) {
|
||||
ensureValidParameterName(parameterName);
|
||||
if (!argumentValue) {
|
||||
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollecti
|
||||
|
||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||
|
||||
public addArgument(argument: IFunctionCallArgument): void {
|
||||
if (!argument) {
|
||||
throw new Error('undefined argument');
|
||||
@@ -12,15 +13,18 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
||||
}
|
||||
this.arguments.set(argument.parameterName, argument);
|
||||
}
|
||||
|
||||
public getAllParameterNames(): string[] {
|
||||
return Array.from(this.arguments.keys());
|
||||
}
|
||||
|
||||
public hasArgument(parameterName: string): boolean {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
}
|
||||
return this.arguments.has(parameterName);
|
||||
}
|
||||
|
||||
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||
if (!parameterName) {
|
||||
throw new Error('undefined parameter name');
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||
import { FunctionCall } from '../FunctionCall';
|
||||
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||
) {
|
||||
}
|
||||
|
||||
public compileCall(
|
||||
calls: IFunctionCall[],
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
functions: ISharedFunctionCollection,
|
||||
): ICompiledCode {
|
||||
if (!functions) { throw new Error('undefined functions'); }
|
||||
if (!calls) { throw new Error('undefined calls'); }
|
||||
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||
@@ -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);
|
||||
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||
if (func.body.code) { // Function with inline code
|
||||
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||
return [compiledCode];
|
||||
} else { // Function with inner calls
|
||||
}
|
||||
// Function with inner calls
|
||||
return func.body.calls
|
||||
.map((innerCall) => {
|
||||
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||
@@ -71,12 +77,12 @@ function compileSingleCall(call: IFunctionCall, context: ICompilationContext): I
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
code: IFunctionCode,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
compiler: IExpressionsCompiler): ICompiledFunctionCall {
|
||||
compiler: IExpressionsCompiler,
|
||||
): ICompiledFunctionCall {
|
||||
return {
|
||||
code: compiler.compileExpressions(code.do, args),
|
||||
revertCode: compiler.compileExpressions(code.revert, args),
|
||||
@@ -90,7 +96,7 @@ function compileArgs(
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const compiledArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
||||
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
|
||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
||||
compiledArgs.addArgument(newArgument);
|
||||
@@ -106,7 +112,8 @@ function merge(codeParts: readonly string[]): string {
|
||||
|
||||
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
func: ISharedFunction,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): void {
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const callArgumentNames = args.getAllParameterNames();
|
||||
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||
@@ -115,7 +122,8 @@ function ensureThatCallArgumentsExistInParameterDefinition(
|
||||
|
||||
function findUnexpectedParameters(
|
||||
callArgumentNames: string[],
|
||||
functionParameterNames: string[]): string[] {
|
||||
functionParameterNames: string[],
|
||||
): string[] {
|
||||
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -126,14 +134,16 @@ function findUnexpectedParameters(
|
||||
function throwIfNotEmpty(
|
||||
functionName: string,
|
||||
unexpectedParameters: string[],
|
||||
expectedParameters: string[]) {
|
||||
expectedParameters: string[],
|
||||
) {
|
||||
if (!unexpectedParameters.length) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: ` +
|
||||
`"${unexpectedParameters.join('", "')}"` +
|
||||
'. Expected parameter(s): ' +
|
||||
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
// eslint-disable-next-line prefer-template
|
||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||
+ `"${unexpectedParameters.join('", "')}"`
|
||||
+ '. Expected parameter(s): '
|
||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||
import { IFunctionCall } from '../IFunctionCall';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
|
||||
@@ -4,7 +4,8 @@ import { IFunctionCall } from './IFunctionCall';
|
||||
export class FunctionCall implements IFunctionCall {
|
||||
constructor(
|
||||
public readonly functionName: string,
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
if (!functionName) {
|
||||
throw new Error('empty function name in function call');
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||
import { FunctionCall } from './FunctionCall';
|
||||
|
||||
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||
if (!calls) {
|
||||
if (calls === undefined) {
|
||||
throw new Error('undefined call data');
|
||||
}
|
||||
const sequence = getCallSequence(calls);
|
||||
@@ -24,7 +24,7 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||
|
||||
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call`);
|
||||
throw new Error('undefined function call');
|
||||
}
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
for (const parameterName of Object.keys(call.parameters || {})) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
|
||||
import { IFunctionParameter } from './IFunctionParameter';
|
||||
|
||||
export class FunctionParameter implements IFunctionParameter {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly isOptional: boolean) {
|
||||
public readonly isOptional: boolean,
|
||||
) {
|
||||
ensureValidParameterName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
||||
public get all(): readonly IFunctionParameter[] {
|
||||
return this.parameters;
|
||||
}
|
||||
|
||||
public addParameter(parameter: IFunctionParameter) {
|
||||
this.ensureValidParameter(parameter);
|
||||
this.parameters.push(parameter);
|
||||
@@ -15,6 +16,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
||||
private includesName(name: string) {
|
||||
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
||||
}
|
||||
|
||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||
if (!parameter) {
|
||||
throw new Error('undefined parameter');
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
|
||||
import { IFunctionCall } from './Call/IFunctionCall';
|
||||
import {
|
||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
||||
} from './ISharedFunction';
|
||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||
|
||||
export function createCallerFunction(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
callSequence: readonly IFunctionCall[]): ISharedFunction {
|
||||
callSequence: readonly IFunctionCall[],
|
||||
): ISharedFunction {
|
||||
if (!callSequence) {
|
||||
throw new Error(`undefined call sequence in function "${name}"`);
|
||||
}
|
||||
@@ -19,7 +22,8 @@ export function createFunctionWithInlineCode(
|
||||
name: string,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
code: string,
|
||||
revertCode?: string): ISharedFunction {
|
||||
revertCode?: string,
|
||||
): ISharedFunction {
|
||||
if (!code) {
|
||||
throw new Error(`undefined code in function "${name}"`);
|
||||
}
|
||||
@@ -32,6 +36,7 @@ export function createFunctionWithInlineCode(
|
||||
|
||||
class SharedFunction implements ISharedFunction {
|
||||
public readonly body: ISharedFunctionBody;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||
@@ -39,7 +44,7 @@ class SharedFunction implements ISharedFunction {
|
||||
bodyType: FunctionBodyType,
|
||||
) {
|
||||
if (!name) { throw new Error('undefined function name'); }
|
||||
if (!parameters) { throw new Error(`undefined parameters`); }
|
||||
if (!parameters) { throw new Error('undefined parameters'); }
|
||||
this.body = {
|
||||
type: bodyType,
|
||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||
|
||||
@@ -11,8 +11,10 @@ import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||
|
||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||
|
||||
public parseFunctions(
|
||||
functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
functions: readonly FunctionData[],
|
||||
): ISharedFunctionCollection {
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
@@ -27,15 +29,15 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||
}
|
||||
|
||||
function parseFunction(data: FunctionData): ISharedFunction {
|
||||
const name = data.name;
|
||||
const { name } = data;
|
||||
const parameters = parseParameters(data);
|
||||
if (hasCode(data)) {
|
||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||
} else { // has call
|
||||
}
|
||||
// Has call
|
||||
const calls = parseFunctionCalls(data.call);
|
||||
return createCallerFunction(name, parameters, calls);
|
||||
}
|
||||
}
|
||||
|
||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||
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)
|
||||
&& value.every((item) => typeof item === 'object');
|
||||
}
|
||||
@@ -115,15 +117,14 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
throw new Error('some functions are undefined');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code),
|
||||
);
|
||||
.filter((code) => code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
|
||||
export interface IScriptCompiler {
|
||||
canCompile(script: ScriptData): boolean;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode, ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||
@@ -12,15 +11,17 @@ import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
|
||||
constructor(
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||
}
|
||||
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
if (!script.call) {
|
||||
@@ -28,6 +29,7 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
try {
|
||||
@@ -36,7 +38,8 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax);
|
||||
this.syntax,
|
||||
);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { parseDocUrls } from '../DocumentationParser';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { parseDocUrls } from '../DocumentationParser';
|
||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
|
||||
export function parseScript(
|
||||
data: ScriptData, context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||
data: ScriptData,
|
||||
context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel),
|
||||
): Script {
|
||||
validateScript(data);
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
const script = new Script(
|
||||
/* name */ data.name,
|
||||
/* code */ parseCode(data, context),
|
||||
/* docs */ parseDocUrls(data),
|
||||
/* level */ parseLevel(data.recommend, levelParser));
|
||||
/* name: */ data.name,
|
||||
/* code: */ parseCode(data, context),
|
||||
/* docs: */ parseDocUrls(data),
|
||||
/* level: */ parseLevel(data.recommend, levelParser),
|
||||
);
|
||||
return script;
|
||||
}
|
||||
|
||||
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
||||
function parseLevel(
|
||||
level: string,
|
||||
parser: IEnumParser<RecommendationLevel>,
|
||||
): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
|
||||
const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
|
||||
const PowerShellCommonCodeParts = ['{', '}'];
|
||||
|
||||
export class BatchFileSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = ['REM', '::'];
|
||||
|
||||
public readonly commonCodeParts = [...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||
|
||||
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
|
||||
}
|
||||
export type ISyntaxFactory = IScriptingLanguageFactory<ILanguageSyntax>;
|
||||
|
||||
@@ -2,5 +2,6 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = ['#'];
|
||||
|
||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { BatchFileSyntax } from './BatchFileSyntax';
|
||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||
|
||||
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
|
||||
export class SyntaxFactory
|
||||
extends ScriptingLanguageFactory<ILanguageSyntax>
|
||||
implements ISyntaxFactory {
|
||||
constructor() {
|
||||
super();
|
||||
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||
|
||||
@@ -3,9 +3,9 @@ import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compile
|
||||
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
|
||||
export class CodeSubstituter implements ICodeSubstituter {
|
||||
constructor(
|
||||
@@ -14,12 +14,13 @@ export class CodeSubstituter implements ICodeSubstituter {
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public substitute(code: string, info: IProjectInformation): string {
|
||||
if (!code) { throw new Error('undefined code'); }
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
const args = new FunctionCallArgumentCollection();
|
||||
const substitute = (name: string, value: string) =>
|
||||
args.addArgument(new FunctionCallArgument(name, value));
|
||||
const substitute = (name: string, value: string) => args
|
||||
.addArgument(new FunctionCallArgument(name, value));
|
||||
substitute('homepage', info.homepage);
|
||||
substitute('version', info.version);
|
||||
substitute('date', this.date.toUTCString());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
@@ -13,9 +13,11 @@ export class ScriptingDefinitionParser {
|
||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||
) {
|
||||
}
|
||||
|
||||
public parse(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation): IScriptingDefinition {
|
||||
info: IProjectInformation,
|
||||
): IScriptingDefinition {
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
if (!definition) { throw new Error('undefined definition'); }
|
||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||
@@ -28,4 +30,3 @@ export class ScriptingDefinitionParser {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -955,7 +955,7 @@ actions:
|
||||
sudo defaults write '/var/db/SystemPolicy-prefs' 'enabled' -string 'no'
|
||||
echo "Disabled Gatekeeper"
|
||||
else
|
||||
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
||||
>&2 echo "Unknown gatekeeper status: $gatekeeper_status"
|
||||
fi
|
||||
fi
|
||||
revertCode: |-
|
||||
@@ -974,7 +974,7 @@ actions:
|
||||
elif [ $gatekeeper_status = "enabled" ]; then
|
||||
echo "No action needed, Gatekeeper is already enabled"
|
||||
else
|
||||
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
||||
>&2 echo "Unknown Gatekeeper status: $gatekeeper_status"
|
||||
fi
|
||||
fi
|
||||
-
|
||||
|
||||
@@ -4,7 +4,10 @@ import { IProjectInformation } from './IProjectInformation';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
|
||||
export class Application implements IApplication {
|
||||
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
|
||||
constructor(
|
||||
public info: IProjectInformation,
|
||||
public collections: readonly ICategoryCollection[],
|
||||
) {
|
||||
validateInformation(info);
|
||||
validateCollections(collections);
|
||||
}
|
||||
@@ -37,8 +40,8 @@ function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||
const osList = collections.map((c) => c.os);
|
||||
const duplicates = getDuplicates(osList);
|
||||
if (duplicates.length > 0) {
|
||||
throw new Error('multiple collections with same os: ' +
|
||||
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "'));
|
||||
throw new Error(`multiple collections with same os: ${
|
||||
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ export class Category extends BaseEntity<number> implements ICategory {
|
||||
public readonly name: string,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||
public readonly scripts?: ReadonlyArray<IScript>,
|
||||
) {
|
||||
super(id);
|
||||
validateCategory(this);
|
||||
}
|
||||
@@ -20,7 +21,10 @@ export class Category extends BaseEntity<number> implements ICategory {
|
||||
}
|
||||
|
||||
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) {
|
||||
throw new Error('undefined or empty name');
|
||||
}
|
||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||
(!category.scripts || category.scripts.length === 0)) {
|
||||
if (
|
||||
(!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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ICategoryCollection } from './ICategoryCollection';
|
||||
|
||||
export class CategoryCollection implements ICategoryCollection {
|
||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||
|
||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||
|
||||
private readonly queryable: IQueryableCollection;
|
||||
@@ -16,7 +17,8 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
constructor(
|
||||
public readonly os: OperatingSystem,
|
||||
public readonly actions: ReadonlyArray<ICategory>,
|
||||
public readonly scripting: IScriptingDefinition) {
|
||||
public readonly scripting: IScriptingDefinition,
|
||||
) {
|
||||
if (!scripting) {
|
||||
throw new Error('undefined scripting definition');
|
||||
}
|
||||
@@ -32,7 +34,7 @@ export class CategoryCollection implements ICategoryCollection {
|
||||
}
|
||||
|
||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||
if (isNaN(level)) {
|
||||
if (level === undefined) {
|
||||
throw new Error('undefined level');
|
||||
}
|
||||
if (!(level in RecommendationLevel)) {
|
||||
@@ -68,7 +70,8 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
if (duplicatedIds.length > 0) {
|
||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||
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(
|
||||
categories: ReadonlyArray<ICategory>,
|
||||
allCategories: ICategory[],
|
||||
allScripts: IScript[]): IQueryableCollection {
|
||||
allScripts: IScript[],
|
||||
): IQueryableCollection {
|
||||
if (!categories || categories.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -126,7 +130,8 @@ function flattenCategories(
|
||||
|
||||
function flattenScripts(
|
||||
scripts: ReadonlyArray<IScript>,
|
||||
allScripts: IScript[]): IScript[] {
|
||||
allScripts: IScript[],
|
||||
): IScript[] {
|
||||
if (!scripts) {
|
||||
return;
|
||||
}
|
||||
@@ -136,7 +141,8 @@ function flattenScripts(
|
||||
}
|
||||
|
||||
function makeQueryable(
|
||||
actions: ReadonlyArray<ICategory>): IQueryableCollection {
|
||||
actions: ReadonlyArray<ICategory>,
|
||||
): IQueryableCollection {
|
||||
const flattened = flattenApplication(actions);
|
||||
return {
|
||||
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[]>();
|
||||
for (const levelName of getEnumNames(RecommendationLevel)) {
|
||||
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);
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
|
||||
export interface IProjectInformation {
|
||||
readonly name: string;
|
||||
readonly version: string;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
import { IProjectInformation } from './IProjectInformation';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
import { assertInRange } from '@/application/Common/Enum';
|
||||
|
||||
export class ProjectInformation implements IProjectInformation {
|
||||
public readonly repositoryWebUrl: string;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly version: string,
|
||||
@@ -24,12 +25,15 @@ export class ProjectInformation implements IProjectInformation {
|
||||
}
|
||||
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
|
||||
}
|
||||
|
||||
public getDownloadUrl(os: OperatingSystem): string {
|
||||
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
|
||||
}
|
||||
|
||||
public get feedbackUrl(): string {
|
||||
return `${this.repositoryWebUrl}/issues`;
|
||||
}
|
||||
|
||||
public get releaseUrl(): string {
|
||||
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ export class Script extends BaseEntity<string> implements IScript {
|
||||
public readonly name: string,
|
||||
public readonly code: IScriptCode,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly level?: RecommendationLevel) {
|
||||
public readonly level?: RecommendationLevel,
|
||||
) {
|
||||
super(name);
|
||||
if (!code) {
|
||||
throw new Error(`undefined code (script: ${name})`);
|
||||
}
|
||||
validateLevel(level);
|
||||
}
|
||||
|
||||
public canRevert(): boolean {
|
||||
return Boolean(this.code.revert);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
syntax: ILanguageSyntax) {
|
||||
syntax: ILanguageSyntax,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
validateCode(execute, syntax);
|
||||
validateRevertCode(revert, execute, syntax);
|
||||
@@ -23,7 +24,7 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
||||
try {
|
||||
validateCode(revertCode, syntax);
|
||||
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) {
|
||||
throw Error(`(revert): ${err.message}`);
|
||||
@@ -32,7 +33,7 @@ function validateRevertCode(revertCode: string, execute: string, syntax: ILangua
|
||||
|
||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`code is empty or undefined`);
|
||||
throw new Error('code is empty or undefined');
|
||||
}
|
||||
ensureNoEmptyLines(code);
|
||||
ensureCodeHasUniqueLines(code, syntax);
|
||||
@@ -61,7 +62,7 @@ function printDuplicatedLines(allLines: string[]) {
|
||||
return allLines
|
||||
.map((line, index) => {
|
||||
const occurrenceIndices = allLines
|
||||
.map((e, i) => e === line ? i : '')
|
||||
.map((e, i) => (e === line ? i : ''))
|
||||
.filter(String);
|
||||
const isDuplicate = occurrenceIndices.length > 1;
|
||||
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
||||
@@ -71,10 +72,12 @@ function printDuplicatedLines(allLines: string[]) {
|
||||
}
|
||||
|
||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||
codeLine = codeLine.toLowerCase();
|
||||
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||
const lowerCaseCodeLine = codeLine.toLowerCase();
|
||||
const isCommentLine = () => syntax.commentDelimiters.some(
|
||||
(delimiter) => lowerCaseCodeLine.startsWith(delimiter),
|
||||
);
|
||||
const consistsOfFrequentCommands = () => {
|
||||
const trimmed = codeLine.trim().split(' ');
|
||||
const trimmed = lowerCaseCodeLine.trim().split(' ');
|
||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||
};
|
||||
return isCommentLine() || consistsOfFrequentCommands();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IScriptingDefinition } from './IScriptingDefinition';
|
||||
|
||||
export class ScriptingDefinition implements IScriptingDefinition {
|
||||
public readonly fileExtension: string;
|
||||
|
||||
constructor(
|
||||
public readonly language: ScriptingLanguage,
|
||||
public readonly startCode: string,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
// eslint-disable-next-line camelcase
|
||||
import child_process from 'child_process';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export class CodeRunner {
|
||||
constructor(
|
||||
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> {
|
||||
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
|
||||
await this.node.fs.promises.mkdir(dir, { recursive: true });
|
||||
@@ -36,13 +39,16 @@ function getExecuteCommand(scriptPath: string, environment: Environment): string
|
||||
}
|
||||
|
||||
function getNodeJs(): INodeJs {
|
||||
return { os, path, fs, child_process };
|
||||
return {
|
||||
os, path, fs, child_process,
|
||||
};
|
||||
}
|
||||
|
||||
export interface INodeJs {
|
||||
os: INodeOs;
|
||||
path: INodePath;
|
||||
fs: INodeFs;
|
||||
// eslint-disable-next-line camelcase
|
||||
child_process: INodeChildProcess;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { IEventSubscription } from './IEventSource';
|
||||
|
||||
export class EventSubscriptionCollection {
|
||||
private readonly subscriptions = new Array<IEventSubscription>();
|
||||
|
||||
public register(...subscriptions: IEventSubscription[]) {
|
||||
this.subscriptions.push(...subscriptions);
|
||||
}
|
||||
|
||||
public unsubscribeAll() {
|
||||
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
||||
this.subscriptions.splice(0, this.subscriptions.length);
|
||||
|
||||
@@ -7,5 +7,3 @@ export interface IEventSubscription {
|
||||
}
|
||||
|
||||
export type EventHandler<T> = (data: T) => void;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IEntity } from '../Entity/IEntity';
|
||||
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[];
|
||||
|
||||
constructor(items?: TEntity[]) {
|
||||
|
||||
@@ -4,11 +4,13 @@ export enum FileType {
|
||||
BatchFile,
|
||||
ShellScript,
|
||||
}
|
||||
|
||||
export class SaveFileDialog {
|
||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||
const mimeType = this.mimeTypes.get(type);
|
||||
this.saveBlob(text, mimeType, fileName);
|
||||
}
|
||||
|
||||
private static readonly mimeTypes = new Map<FileType, string>([
|
||||
// Some browsers (including firefox + IE) require right mime type
|
||||
// otherwise they ignore extension and save the file as text.
|
||||
|
||||
@@ -2,8 +2,11 @@ import { EventSource } from '../Events/EventSource';
|
||||
|
||||
export class AsyncLazy<T> {
|
||||
private valueCreated = new EventSource();
|
||||
|
||||
private isValueCreated = false;
|
||||
|
||||
private isCreatingValue = false;
|
||||
|
||||
private value: T | undefined;
|
||||
|
||||
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 (this.isCreatingValue) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
return new Promise<T>((resolve) => {
|
||||
// Return/result when valueCreated event is triggered.
|
||||
this.valueCreated.on(() => resolve(this.value));
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
return new Promise((resolve) => scheduler(() => resolve(undefined), time));
|
||||
return new Promise((resolve) => {
|
||||
scheduler(() => resolve(undefined), time);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,14 +7,13 @@ import { TooltipBootstrapper } from './Modules/TooltipBootstrapper';
|
||||
|
||||
export class ApplicationBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.config.productionTip = false;
|
||||
const bootstrappers = this.getAllBootstrappers();
|
||||
const bootstrappers = ApplicationBootstrapper.getAllBootstrappers();
|
||||
for (const bootstrapper of bootstrappers) {
|
||||
bootstrapper.bootstrap(vue);
|
||||
}
|
||||
}
|
||||
|
||||
private getAllBootstrappers(): IVueBootstrapper[] {
|
||||
private static getAllBootstrappers(): IVueBootstrapper[] {
|
||||
return [
|
||||
new IconBootstrapper(),
|
||||
new TreeBootstrapper(),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { IVueBootstrapper, VueConstructor } from './../IVueBootstrapper';
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
/** BRAND ICONS (PREFIX: fab) */
|
||||
@@ -6,8 +5,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
/** REGULAR ICONS (PREFIX: far) */
|
||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
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 {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
@@ -21,11 +23,13 @@ export class IconBootstrapper implements IVueBootstrapper {
|
||||
faFolderOpen,
|
||||
faFolder,
|
||||
faTimes,
|
||||
faFileDownload, faSave,
|
||||
faFileDownload,
|
||||
faSave,
|
||||
faCopy,
|
||||
faPlay,
|
||||
faSearch,
|
||||
faBatteryFull, faBatteryHalf,
|
||||
faBatteryFull,
|
||||
faBatteryHalf,
|
||||
faInfoCircle,
|
||||
faArrowsAltH,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
|
||||
export class TooltipBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LiquorTree from 'liquor-tree';
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
|
||||
export class TreeBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import VModal from 'vue-js-modal';
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
|
||||
export class VModalBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { VueConstructor, IVueBootstrapper } from './../IVueBootstrapper';
|
||||
import { VueConstructor, IVueBootstrapper } from '../IVueBootstrapper';
|
||||
|
||||
export class VueBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
vue.config.productionTip = false;
|
||||
const { config } = vue;
|
||||
config.productionTip = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||
import {
|
||||
Component, Prop, Emit, Vue,
|
||||
} from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class IconButton extends Vue {
|
||||
@Prop() public text!: number;
|
||||
|
||||
@Prop() public iconPrefix!: string;
|
||||
|
||||
@Prop() public iconName!: string;
|
||||
|
||||
@Emit('click')
|
||||
public onClicked() {
|
||||
return;
|
||||
}
|
||||
@Emit('click') public onClicked() { /* do nothing except firing event */ }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,5 +64,4 @@ export default class IconButton extends Vue {
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="instructions">
|
||||
<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>
|
||||
<ol>
|
||||
@@ -10,7 +11,9 @@
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
: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>
|
||||
@@ -18,7 +21,8 @@
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
: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>
|
||||
@@ -29,10 +33,11 @@
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="
|
||||
'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/>' +
|
||||
'• `cd` will change the current folder.<br/>' +
|
||||
'• `~` is the user home directory.'"
|
||||
'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/>'
|
||||
+ '• `cd` will change the current folder.<br/>'
|
||||
+ '• `~` is the user home directory.'"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -74,16 +79,17 @@
|
||||
</ol>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import Code from './Code.vue';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
import Code from './Code.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -92,7 +98,9 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
})
|
||||
export default class MacOsInstructions extends Vue {
|
||||
@Prop() public fileName: string;
|
||||
|
||||
public appName = '';
|
||||
|
||||
public macOsDownloadUrl = '';
|
||||
|
||||
public async created() {
|
||||
|
||||
@@ -29,8 +29,6 @@ import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
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 { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
@@ -39,6 +37,8 @@ import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
||||
import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import MacOsInstructions from './MacOsInstructions.vue';
|
||||
import IconButton from './IconButton.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -49,39 +49,47 @@ import { IReadOnlyApplicationContext } from '@/application/Context/IApplicationC
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
|
||||
public canRun = false;
|
||||
|
||||
public hasCode = false;
|
||||
|
||||
public isMacOsCollection = false;
|
||||
|
||||
public fileName = '';
|
||||
|
||||
public async copyCode() {
|
||||
const code = await this.getCurrentCode();
|
||||
Clipboard.copyText(code.current);
|
||||
}
|
||||
|
||||
public async saveCode() {
|
||||
const context = await this.getCurrentContext();
|
||||
saveCode(this.fileName, context.state);
|
||||
if (this.isMacOsCollection) {
|
||||
(this.$refs.instructionsDialog as any).show();
|
||||
(this.$refs.instructionsDialog as Dialog).show();
|
||||
}
|
||||
}
|
||||
|
||||
public async executeCode() {
|
||||
const context = await this.getCurrentContext();
|
||||
await executeCode(context);
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
|
||||
this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os;
|
||||
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||
const isNewOs = (test: OperatingSystem) => newState.collection.os === test;
|
||||
this.canRun = this.isDesktopVersion && isNewOs(Environment.CurrentEnvironment.os);
|
||||
this.isMacOsCollection = isNewOs(OperatingSystem.macOS);
|
||||
this.fileName = buildFileName(newState.collection.scripting);
|
||||
this.react(newState.code);
|
||||
}
|
||||
|
||||
private async getCurrentCode(): Promise<IApplicationCode> {
|
||||
const context = await this.getCurrentContext();
|
||||
const code = context.state.code;
|
||||
const { code } = context.state;
|
||||
return code;
|
||||
}
|
||||
|
||||
private async react(code: IApplicationCode) {
|
||||
this.hasCode = code.current && code.current.length > 0;
|
||||
this.events.unsubscribeAll();
|
||||
@@ -118,9 +126,9 @@ function buildFileName(scripting: IScriptingDefinition) {
|
||||
async function executeCode(context: IReadOnlyApplicationContext) {
|
||||
const runner = new CodeRunner();
|
||||
await runner.runCode(
|
||||
/*code*/ context.state.code.current,
|
||||
/*appName*/ context.app.info.name,
|
||||
/*fileExtension*/ context.state.collection.scripting.fileExtension,
|
||||
/* code: */ context.state.code.current,
|
||||
/* appName: */ context.app.info.name,
|
||||
/* fileExtension: */ context.state.collection.scripting.fileExtension,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
|
||||
private currentMarkerId?: number;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
@@ -38,6 +39,7 @@ export default class TheCodeArea extends StatefulVue {
|
||||
public destroyed() {
|
||||
this.destroyEditor();
|
||||
}
|
||||
|
||||
public sizeChanged() {
|
||||
if (this.editor) {
|
||||
this.editor.resize();
|
||||
@@ -46,9 +48,14 @@ export default class TheCodeArea extends StatefulVue {
|
||||
|
||||
protected handleCollectionState(newState: IReadOnlyCategoryCollectionState): void {
|
||||
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;
|
||||
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.register(appCode.changed.on((code) => this.updateCode(code)));
|
||||
}
|
||||
@@ -68,6 +75,7 @@ export default class TheCodeArea extends StatefulVue {
|
||||
this.reactToChanges(event, event.changedScripts);
|
||||
}
|
||||
}
|
||||
|
||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||
const positions = scripts
|
||||
.map((script) => event.getScriptPositionInCode(script));
|
||||
@@ -80,16 +88,21 @@ export default class TheCodeArea extends StatefulVue {
|
||||
this.scrollToLine(end + 2);
|
||||
this.highlight(start, end);
|
||||
}
|
||||
|
||||
private highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
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) {
|
||||
const column = this.editor.session.getLine(row).length;
|
||||
this.editor.gotoLine(row, column, true);
|
||||
}
|
||||
|
||||
private removeCurrentHighlighting() {
|
||||
if (!this.currentMarkerId) {
|
||||
return;
|
||||
@@ -97,6 +110,7 @@ export default class TheCodeArea extends StatefulVue {
|
||||
this.editor.session.removeMarker(this.currentMarkerId);
|
||||
this.currentMarkerId = undefined;
|
||||
}
|
||||
|
||||
private destroyEditor() {
|
||||
if (this.editor) {
|
||||
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';
|
||||
const editor = ace.edit(editorId);
|
||||
const lang = getLanguage(language);
|
||||
|
||||
@@ -10,8 +10,8 @@ import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import 'ace-builds/src-noconflict/mode-batchfile';
|
||||
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/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/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/json_worker', new URL('ace-builds/src-noconflict/worker-json.js', import.meta.url).toString());
|
||||
|
||||
export default ace;
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
@Component({
|
||||
@@ -16,8 +18,10 @@ import { NonCollapsing } from '@/presentation/components/Scripts/View/Cards/NonC
|
||||
})
|
||||
export default class MenuOptionListItem extends Vue {
|
||||
@Prop() public enabled: boolean;
|
||||
|
||||
@Prop() public label: string;
|
||||
@Emit('click') public onClicked() { return; }
|
||||
|
||||
@Emit('click') public onClicked() { /* do nothing except firing event */ }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export class SelectionTypeHandler {
|
||||
constructor(private readonly state: ICategoryCollectionState) {
|
||||
if (!state) { throw new Error('undefined state'); }
|
||||
}
|
||||
|
||||
public selectType(type: SelectionType) {
|
||||
if (type === SelectionType.Custom) {
|
||||
throw new Error('cannot select custom type');
|
||||
@@ -23,6 +24,7 @@ export class SelectionTypeHandler {
|
||||
const selector = selectors.get(type);
|
||||
selector.select(this.state);
|
||||
}
|
||||
|
||||
public getCurrentSelectionType(): SelectionType {
|
||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||
if (selector.isSelected(this.state)) {
|
||||
@@ -40,18 +42,14 @@ interface ISingleTypeHandler {
|
||||
|
||||
const selectors = new Map<SelectionType, ISingleTypeHandler>([
|
||||
[SelectionType.None, {
|
||||
select: (state) =>
|
||||
state.selection.deselectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.selectedScripts.length === 0,
|
||||
select: (state) => state.selection.deselectAll(),
|
||||
isSelected: (state) => state.selection.selectedScripts.length === 0,
|
||||
}],
|
||||
[SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)],
|
||||
[SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)],
|
||||
[SelectionType.All, {
|
||||
select: (state) =>
|
||||
state.selection.selectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.selectedScripts.length === state.collection.totalScripts,
|
||||
select: (state) => state.selection.selectAll(),
|
||||
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 selectedScripts = state.selection.selectedScripts;
|
||||
const { selectedScripts } = state.selection;
|
||||
return areAllSelected(scripts, selectedScripts);
|
||||
}
|
||||
|
||||
@@ -75,12 +76,14 @@ function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState)
|
||||
|
||||
function areAllSelected(
|
||||
expectedScripts: ReadonlyArray<IScript>,
|
||||
selection: ReadonlyArray<SelectedScript>): boolean {
|
||||
selection = selection.filter((selected) => !selected.revert);
|
||||
if (expectedScripts.length < selection.length) {
|
||||
selection: ReadonlyArray<SelectedScript>,
|
||||
): boolean {
|
||||
const selectedScriptIds = selection
|
||||
.filter((selected) => !selected.revert)
|
||||
.map((script) => script.id);
|
||||
if (expectedScripts.length < selectedScriptIds.length) {
|
||||
return false;
|
||||
}
|
||||
const selectedScriptIds = selection.map((script) => script.id);
|
||||
const expectedScriptIds = expectedScripts.map((script) => script.id);
|
||||
return scrambledEqual(selectedScriptIds, expectedScriptIds);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user