Compare commits

...

11 Commits

Author SHA1 Message Date
undergroundwires
448e378dc4 increase performance by polyfilling ResizeObserver only if required 2021-03-25 13:24:19 +01:00
undergroundwires
ac2249f256 refactor features to use shared functions #41 2021-03-24 15:29:39 +01:00
undergroundwires
05932c5a36 fix safari cleanup scripts that are not working on modern versions 2021-03-23 19:06:20 +01:00
undergroundwires
6f46cdb4ed refactor all modals to use same dialog component 2021-03-20 16:13:25 +01:00
undergroundwires
5f527a00cf fix fs module hanging not allowing code to run
Run button on Windows stopped working as CodeRunner was hanging when
executing fs.promises.mkdir as described in electron/electron#20951
It started happening after electron update to v12 in 1f515e7.
This commit adds the workaround suggested in electron/electron#19554
that fixes the issue.
2021-03-14 17:26:56 +01:00
undergroundwires
1935db1019 fix throttle function not being able to run with argument(s) 2021-03-13 12:54:13 +01:00
undergroundwires
1f515e7be5 bump dependencies to latest
- fix npm vulnerabilities #62
- change ResizeObserver polyfill dependency que-etc/resize-observer-polyfill#80
- bump typescript to 4.2.x and add tslib for importing helpers
- update electron to v12.x and set contextIsolation to false (nklayman/vue-cli-plugin-electron-builder#1285, electron/electron#11608) to reach node APIs as it's now disabled by default (electron/electron#27949)
2021-03-11 14:50:35 +01:00
undergroundwires
1a5f92021f fix a test where "it" is not used inside "describe" 2021-03-08 17:21:11 +01:00
undergroundwires
f3c7413f52 restructure presentation layer
- Move most GUI related code to /presentation
- Move components to /components (separate from bootstrap and style)
- Move shared components helpers to /components/shared
- Rename Bootstrapping to bootstrapping to enforce same naming
  convention in /presentation
2021-03-07 19:37:54 +01:00
undergroundwires
646db90585 refactor script compilation to make it easy to add new expressions #41 #53 2021-03-05 15:52:49 +01:00
undergroundwires-bot
1f8a0cf9ab ⬆️ bump everywhere to 0.10.0 2021-03-02 16:08:31 +00:00
119 changed files with 3289 additions and 2073 deletions

View File

@@ -1,5 +1,22 @@
# Changelog
## 0.10.0 (2021-03-02)
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)
* add option to run script directly in desktop app | [9a6b903](https://github.com/undergroundwires/privacy.sexy/commit/9a6b903b9297802845043fd41115756acd4a145c)
* add script to automatically kill devicecensus process | [c9b91f6](https://github.com/undergroundwires/privacy.sexy/commit/c9b91f6d8f9bd16308b6beda119e7154a985b6cf)
* refactor disabling application experience and document better | [45a3669](https://github.com/undergroundwires/privacy.sexy/commit/45a3669443d82855a52f60524d341c15f380f9e7)
* escape printed characters to prevent command injection #45 | [1260eea](https://github.com/undergroundwires/privacy.sexy/commit/1260eea690e4fa5420e58c9de9f88cc29cb242db)
* move code area to right on bigger screens | [cf39e6d](https://github.com/undergroundwires/privacy.sexy/commit/cf39e6d2541ea547f41d9553c380c54c24c58038)
* more scripts to disable speech recognition and Cortana | [ee43fd9](https://github.com/undergroundwires/privacy.sexy/commit/ee43fd92a019ebd26c13890f9146c5b5bb56afaf)
* add more macos scripts for privacy cleanup | [b0a7d0b](https://github.com/undergroundwires/privacy.sexy/commit/b0a7d0b53b3d8ac144a0241d70c037f460b0c0cc)
* add better error messages to setting vscode settings | [65226f3](https://github.com/undergroundwires/privacy.sexy/commit/65226f3984480d0bc7932fd8d76a328f08308850)
* remove windows scripts for removing non-bloating system apps #55 | [15004ff](https://github.com/undergroundwires/privacy.sexy/commit/15004ff1f1fb85a1d92e11ef695bcb2f37110610)
* remove "preview" disclaimer from macOS | [970221b](https://github.com/undergroundwires/privacy.sexy/commit/970221b996e25fe5b029cbaa78607c9bbc8c3c0e)
* update screenshot | [bd41af4](https://github.com/undergroundwires/privacy.sexy/commit/bd41af466fd135f7dc2f171633e4f60d8547c373)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.2...0.10.0)
## 0.9.2 (2021-02-13)
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)

View File

@@ -7,9 +7,9 @@
- Proposing new features
- Becoming a maintainer
## Pull Request Process
## Pull request process
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) with [GitOps](./img/architecture/gitops.png) is used
- Your pull requests are actively welcomed.
- The steps:
1. Fork the repo and create your branch from master.
@@ -25,4 +25,10 @@
## License
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
By contributing, you agree that your contributions will be licensed under its [GNU General Public License v3.0](./LICENSE).
## Read more
- See [tests](./docs/tests.md) for testing
- See [extend script](./README.md#extend-scripts) for quick steps to extend scripts
- See [architecture overview](./README.md#architecture-overview) to deep dive into privacy.sexy codebase

View File

@@ -16,7 +16,7 @@
- Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage).
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-Setup-0.10.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.0/privacy.sexy-0.10.0.AppImage).
- 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -53,8 +53,8 @@
- Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.2 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.2 undergroundwires/privacy.sexy:0.9.2`
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.0 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.0 undergroundwires/privacy.sexy:0.10.0`
## Architecture overview

View File

@@ -2,7 +2,7 @@
- It's mainly responsible for
- creating and event based [application state](#application-state)
- parsing and compiling [application data](#application-data)
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
## Application state
@@ -14,9 +14,23 @@
## Application data
- Compiled to `Application` domain object.
- Compiled to [`Application`](./../src/domain/Application.ts) domain object.
- The scripts are defined and controlled in different data files per OS
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
- Application data is defined in collection files and
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
## Parsing
- Application data is parsed to domain object [`Application.ts`](./../src/domain/Application.ts)
- Steps
1. (Compile time) Load application data from [collection yaml files](./../src/application/collections/) using webpack loader
2. (Runtime) Parse and compile application and make it available to presentation layer by [`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)
### Compiling
- Parsing the application files includes compiling scripts using [collection file defined functions](./collection-files.md#function)
- To extend the syntax:
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts)

View File

@@ -4,6 +4,20 @@
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly.
## Structure
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
- [**`background.ts`**](./../src/presentation/background.ts): Main process of Electron, started as first thing when app starts.
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
## Application data
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
@@ -16,9 +30,22 @@
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
- Stateless components that does not handle state extends `Vue`
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/StatefulVue.ts)
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/StatefulVue.ts)
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/StatefulVue.ts) makes lifecycling easier
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) makes lifecycling easier
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
## Modals
- [Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that can be used to show modal windows
- Simply wrap the content inside of its slot and call `.show()` method on its reference.
- Example:
```html
<Dialog ref="testDialog">
<div>Hello world</div>
</Dialog>
<div @click="$refs.testDialog.show()">Show dialog</div>
```

2544
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
"version": "0.9.2",
"version": "0.10.0",
"private": true,
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"author": "undergroundwires",
@@ -21,48 +21,49 @@
},
"main": "background.js",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-brands-svg-icons": "^5.15.1",
"@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/free-regular-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^2.0.2",
"@juggle/resize-observer": "^3.3.0",
"ace-builds": "^1.4.12",
"core-js": "^3.6.5",
"core-js": "^3.9.1",
"file-saver": "^2.0.5",
"inversify": "^5.0.5",
"liquor-tree": "^0.2.70",
"resize-observer-polyfill": "^1.5.1",
"v-tooltip": "2.0.2",
"v-tooltip": "2.1.2",
"vue": "^2.6.12",
"vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^9.1.2"
},
"devDependencies": {
"@types/ace": "0.0.44",
"@types/chai": "^4.2.14",
"@types/ace": "0.0.45",
"@types/chai": "^4.2.15",
"@types/file-saver": "^2.0.1",
"@types/mocha": "^8.2.0",
"@vue/cli-plugin-babel": "^4.5.10",
"@vue/cli-plugin-typescript": "^4.5.9",
"@vue/cli-plugin-unit-mocha": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"@vue/test-utils": "1.1.2",
"chai": "^4.2.0",
"electron": "^11.1.0",
"@types/mocha": "^8.2.1",
"@vue/cli-plugin-babel": "^4.5.11",
"@vue/cli-plugin-typescript": "^4.5.11",
"@vue/cli-plugin-unit-mocha": "^4.5.11",
"@vue/cli-service": "^4.5.11",
"@vue/test-utils": "1.1.3",
"chai": "^4.3.3",
"electron": "^12.0.1",
"electron-devtools-installer": "^3.1.1",
"electron-log": "^4.3.1",
"electron-updater": "^4.3.5",
"electron-log": "^4.3.2",
"electron-updater": "^4.3.8",
"js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.26.0",
"markdownlint-cli": "^0.27.1",
"remark-cli": "^9.0.0",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^4.0.0",
"remark-validate-links": "^10.0.2",
"sass": "^1.30.0",
"sass-loader": "^10.1.0",
"typescript": "^4.1.3",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
"remark-validate-links": "^10.0.3",
"sass": "^1.32.8",
"sass-loader": "^10.1.1",
"tslib": "^2.1.0",
"typescript": "^4.2.3",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
"vue-template-compiler": "^2.6.12",
"yaml-lint": "^1.2.4"
},

View File

@@ -2,19 +2,20 @@ import { Category } from '@/domain/Category';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseCategory } from './CategoryParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
import { createEnumParser } from '../Common/Enum';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
export function parseCategoryCollection(
content: CollectionData,
info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
validate(content);
const scripting = parseScriptingDefinition(content.scripting, info);
const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info);
const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = new Array<Category>();
for (const action of content.actions) {

View File

@@ -0,0 +1,35 @@
import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionArguments, IExpression } from './IExpression';
export type ExpressionEvaluator = (args?: ExpressionArguments) => string;
export class Expression implements IExpression {
constructor(
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator,
public readonly parameters: readonly string[] = new Array<string>()) {
if (!position) {
throw new Error('undefined position');
}
if (!evaluator) {
throw new Error('undefined evaluator');
}
}
public evaluate(args?: ExpressionArguments): string {
args = filterUnusedArguments(this.parameters, args);
return this.evaluator(args);
}
}
function filterUnusedArguments(
parameters: readonly string[], args: ExpressionArguments): ExpressionArguments {
let result: ExpressionArguments = {};
for (const parameter of Object.keys(args)) {
if (parameters.includes(parameter)) {
result = {
...result,
[parameter]: args[parameter],
};
}
}
return result;
}

View File

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

View File

@@ -0,0 +1,12 @@
import { ExpressionPosition } from './ExpressionPosition';
export interface IExpression {
readonly position: ExpressionPosition;
readonly parameters?: readonly string[];
evaluate(args?: ExpressionArguments): string;
}
export interface ExpressionArguments {
readonly [parameter: string]: string;
}

View File

@@ -1,31 +1,49 @@
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
import { generateIlCode, IILCode } from './ILCode';
import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
export class ExpressionsCompiler implements IExpressionsCompiler {
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
protected constructor() { }
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
let intermediateCode = generateIlCode(code);
intermediateCode = substituteParameters(intermediateCode, parameters);
return intermediateCode.compile();
const expressions = this.extractor.findExpressions(code);
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat();
const uniqueParameterNames = Array.from(new Set(requiredParameterNames));
ensureRequiredArgsProvided(uniqueParameterNames, parameters);
return compileExpressions(expressions, code, parameters);
}
}
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
const parameterNames = intermediateCode.getUniqueParameterNames();
ensureValuesProvided(parameterNames, parameters);
for (const parameterName of parameterNames) {
const parameterValue = parameters[parameterName];
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) {
let compiledCode = '';
expressions = expressions
.slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start);
let index = 0;
while (index !== code.length) {
const nextExpression = expressions.pop();
if (nextExpression) {
compiledCode += code.substring(index, nextExpression.position.start);
const expressionCode = nextExpression.evaluate(parameters);
compiledCode += expressionCode;
index = nextExpression.position.end;
} else {
compiledCode += code.substring(index, code.length);
break;
}
}
return intermediateCode;
return compiledCode;
}
function ensureValuesProvided(names: string[], nameValues: ParameterValueDictionary) {
nameValues = nameValues || {};
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
if (notProvidedNames.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) {
parameters = parameters || [];
args = args || {};
if (!parameters.length) {
return;
}
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter]));
if (notProvidedParameters.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`);
}
}

View File

@@ -1,73 +0,0 @@
export interface IILCode {
compile(): string;
getUniqueParameterNames(): string[];
substituteParameter(parameterName: string, parameterValue: string): IILCode;
}
export function generateIlCode(rawText: string): IILCode {
const ilCode = generateIl(rawText);
return new ILCode(ilCode);
}
class ILCode implements IILCode {
private readonly ilCode: string;
constructor(ilCode: string) {
this.ilCode = ilCode;
}
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
return new ILCode(newCode);
}
public getUniqueParameterNames(): string[] {
return getUniqueParameterNames(this.ilCode);
}
public compile(): string {
ensureNoExpressionLeft(this.ilCode);
return this.ilCode;
}
}
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
function generateIl(rawText: string): string {
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
return `\{\{exp|${match.trim()}\}\}`;
});
}
// finds all "{{exp|..}} left"
function ensureNoExpressionLeft(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
const uniqueExpressions = getDistinctValues(allMatches);
if (uniqueExpressions.length > 0) {
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
}
}
// Parses all distinct usages of {{exp|$parameterName}}
function getUniqueParameterNames(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
const uniqueParameterNames = getDistinctValues(allParameters);
return uniqueParameterNames;
}
// substitutes {{exp|$parameterName}} to value of the parameter
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
const pattern = `{{exp|$${parameterName}}}`;
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
}
function getDistinctValues(values: readonly string[]): string[] {
return values.filter((value, index, self) => {
return self.indexOf(value) === index;
});
}
function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
import { IExpressionParser } from './IExpressionParser';
import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { IExpression } from '../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../Expression/Expression';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
const matches = Array.from(code.matchAll(this.regex));
for (const match of matches) {
const startPos = match.index;
const endPos = startPos + match[0].length;
let position: ExpressionPosition;
try {
position = new ExpressionPosition(startPos, endPos);
} catch (error) {
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
}
const primitiveExpression = this.buildExpression(match);
const expression = new Expression(position, primitiveExpression.evaluator, primitiveExpression.parameters);
yield expression;
}
}
}
export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator;
parameters?: readonly string[];
}

View File

@@ -0,0 +1,12 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = /{{\s*\$\s*([^}| ]+)\s*}}/g;
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
return {
parameters: [ parameterName ],
evaluator: (args) => args[parameterName],
};
}
}

View File

@@ -49,6 +49,7 @@ function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoDuplicatesInParameterNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParameterNameTypes(functions);
}
function printList(list: readonly string[]): string {
@@ -67,6 +68,17 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[])
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
}
}
function ensureExpectedParameterNameTypes(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions.filter((func) => func.parameters && !isArrayOfStrings(func.parameters));
if (unexpectedFunctions.length) {
throw new Error(`unexpected parameter name type in ${printNames(unexpectedFunctions)}`);
}
function isArrayOfStrings(value: any): boolean {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
}
function printNames(holders: readonly InstructionHolder[]) {
return printList(holders.map((holder) => holder.name));
}

View File

@@ -8,7 +8,7 @@ import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { }
public compileCall(
call: ScriptFunctionCallData,
functions: ISharedFunctionCollection): ICompiledCode {
@@ -32,11 +32,12 @@ export class FunctionCallCompiler implements IFunctionCallCompiler {
}
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
if (!func.parameters && !call.parameters) {
const actual = Object.keys(call.parameters || {});
const expected = func.parameters || [];
if (!actual.length && !expected.length) {
return;
}
const unexpectedParameters = Object.keys(call.parameters || {})
.filter((callParam) => !func.parameters.includes(callParam));
const unexpectedParameters = actual.filter((callParam) => !expected.includes(callParam));
if (unexpectedParameters.length) {
throw new Error(
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);

View File

@@ -0,0 +1,33 @@
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
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';
export class CodeSubstituter implements ICodeSubstituter {
constructor(
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
private readonly date = new Date(),
) {
}
public substitute(code: string, info: IProjectInformation): string {
if (!code) { throw new Error('undefined code'); }
if (!info) { throw new Error('undefined info'); }
const parameters: ParameterValueDictionary = {
homepage: info.homepage,
version: info.version,
date: this.date.toUTCString(),
};
const compiledCode = this.compiler.compileExpressions(code, parameters);
return compiledCode;
}
}
function createSubstituteCompiler(): IExpressionsCompiler {
const parsers = [ new ParameterSubstitutionParser() ];
const parser = new CompositeExpressionParser(parsers);
const expressionCompiler = new ExpressionsCompiler(parser);
return expressionCompiler;
}

View File

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

View File

@@ -0,0 +1,31 @@
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { createEnumParser } from '../../Common/Enum';
import { ICodeSubstituter } from './ICodeSubstituter';
import { CodeSubstituter } from './CodeSubstituter';
export class ScriptingDefinitionParser {
constructor(
private readonly languageParser = createEnumParser(ScriptingLanguage),
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
) {
}
public parse(
definition: ScriptingDefinitionData,
info: IProjectInformation): IScriptingDefinition {
if (!info) { throw new Error('undefined info'); }
if (!definition) { throw new Error('undefined definition'); }
const language = this.languageParser.parseEnum(definition.language, 'language');
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
return new ScriptingDefinition(
language,
startCode,
endCode,
);
}
}

View File

@@ -1,36 +0,0 @@
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { createEnumParser } from '../Common/Enum';
import { generateIlCode } from './Script/Compiler/Expressions/ILCode';
export function parseScriptingDefinition(
definition: ScriptingDefinitionData,
info: IProjectInformation,
date = new Date(),
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
if (!info) {
throw new Error('undefined info');
}
if (!definition) {
throw new Error('undefined definition');
}
const language = languageParser.parseEnum(definition.language, 'language');
const startCode = applySubstitutions(definition.startCode, info, date);
const endCode = applySubstitutions(definition.endCode, info, date);
return new ScriptingDefinition(
language,
startCode,
endCode,
);
}
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
let ilCode = generateIlCode(code);
ilCode = ilCode.substituteParameter('homepage', info.homepage);
ilCode = ilCode.substituteParameter('version', info.version);
ilCode = ilCode.substituteParameter('date', date.toUTCString());
return ilCode.compile();
}

View File

@@ -83,41 +83,78 @@ actions:
children:
-
name: Clear Safari browsing history
docs:
- https://discussions.apple.com/thread/7586106?answerId=30314600022#30314600022
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
code: |-
rm -f ~/Library/Safari/History.plist
rm -f ~/Library/Safari/HistoryIndex.sk
rm -f ~/Library/Safari/History.db
rm -f ~/Library/Safari/History.db-lock
rm -f ~/Library/Safari/History.db-shm
rm -f ~/Library/Safari/History.db-wal
# For older versions of Safari
rm -f ~/Library/Safari/History.plist # URL, visit count, webpage title, last visited timestamp, redirected URL, autocomplete
rm -f ~/Library/Safari/HistoryIndex.sk # History index
-
name: Clear Safari downloads history
docs: https://blog.d204n6.com/2020/09/macos-safari-preferences-and-privacy.html
code: rm -f ~/Library/Safari/Downloads.plist
-
name: Clear Safari top sites
code: rm -f ~/Library/Safari/TopSites.plist
docs: https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
code: rm -f ~/Library/Safari/TopSites.plist
-
name: Clear Safari last session history
name: Clear Safari last session (open tabs) history
docs:
- https://apple.stackexchange.com/a/374116
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-7127
code: rm -f ~/Library/Safari/LastSession.plist
-
name: Clear Safari caches
code: |-
rm -f ~/Library/Caches/com.apple.Safari/Cache.db
rm -f ~/Library/Safari/WebpageIcons.db
rm -rf ~/Library/Caches/com.apple.Safari/Webpage Previews
category: Clear Safari caches
children:
-
name: Clear Safari cached blobs, URLs and timestamps
docs: https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
code: rm -f ~/Library/Caches/com.apple.Safari/Cache.db
-
name: Clear Safari web page icons displayed on URL bar
docs:
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
- https://lifehacker.com/safaris-private-browsing-mode-saves-urls-in-an-easily-a-1691944343
code: rm -f ~/Library/Safari/WebpageIcons.db
-
name: Clear Safari webpage previews (thumbnails)
docs:
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
- https://www.reddit.com/r/apple/comments/18lp92/your_apple_computer_keeps_a_screen_shot_of_nearly/
code: rm -rfv ~/Library/Caches/com.apple.Safari/Webpage\ Previews
-
name: Clear copy of the Safari history
code: rm -rf ~/Library/Caches/Metadata/Safari/History
docs: https://forensicsfromthesausagefactory.blogspot.com/2010/06/safari-history-spotlight-webhistory.html
code: rm -rfv ~/Library/Caches/Metadata/Safari/History
-
name: Clear search history embedded in Safari preferences
docs: https://krypted.com/tag/recentsearchstrings/
code: defaults write ~/Library/Preferences/com.apple.Safari RecentSearchStrings '( )'
-
name: Clear Safari cookies
code: rm -f ~/Library/Cookies/Cookies.plists
docs:
- https://www.toolbox.com/tech/operating-systems/blogs/understanding-the-safari-cookiesbinarycookies-file-format-010712/
- https://link.springer.com/content/pdf/10.1007/0-387-36891-4_13.pdf
code: |-
rm -f ~/Library/Cookies/Cookies.binarycookies
# Used before Safari 5.1
rm -f ~/Library/Cookies/Cookies.plist
-
name: Clear Safari zoom level preferences per site
code: rm -f ~/Library/Safari/PerSiteZoomPreferences.plists
docs: https://blog.d204n6.com/2020/09/macos-safari-preferences-and-privacy.html
code: rm -f ~/Library/Safari/PerSiteZoomPreferences.plist
-
name: Clear URLs that are allowed to display notifications in Safari
docs: https://blog.d204n6.com/2020/09/macos-safari-preferences-and-privacy.html
code: rm -f ~/Library/Safari/UserNotificationPreferences.plist
-
name: Clear Safari per-site preferences for Downloads, Geolocation, PopUps, and Autoplays
docs: https://blog.d204n6.com/2020/09/macos-safari-preferences-and-privacy.html
code: rm -f ~/Library/Safari/PerSitePreferences.db
-
category: Clear Firefox history

View File

@@ -3776,22 +3776,31 @@ actions:
children:
-
name: Direct Play feature
code: dism /Online /Disable-Feature /FeatureName:"DirectPlay" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"DirectPlay" /NoRestart
call:
function: DisableFeature
parameters:
featureName: DirectPlay
-
name: Internet Explorer feature
code: |-
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-x64" /NoRestart
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-x84" /NoRestart
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-amd64" /NoRestart
revertCode: |-
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-x64" /NoRestart
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-x84" /NoRestart
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-amd64" /NoRestart
call:
-
function: DisableFeature
parameters:
featureName: Internet-Explorer-Optional-x64
-
function: DisableFeature
parameters:
featureName: Internet-Explorer-Optional-x84
-
function: DisableFeature
parameters:
featureName: Internet-Explorer-Optional-amd64
-
name: Legacy Components feature
code: dism /Online /Disable-Feature /FeatureName:"LegacyComponents" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"LegacyComponents" /NoRestart
call:
function: DisableFeature
parameters:
featureName: LegacyComponents
-
category: Server features for developers & administrators
children:
@@ -3800,39 +3809,55 @@ actions:
children:
-
name: Hyper-V feature
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-All" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-All" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Microsoft-Hyper-V-All
-
name: Hyper-V GUI Management Tools feature
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Management-Clients" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Management-Clients" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Microsoft-Hyper-V-Management-Clients
-
name: Hyper-V Management Tools feature
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Tools-All" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Tools-All" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Microsoft-Hyper-V-Tools-All
-
name: Hyper-V Module for Windows PowerShell feature
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Management-PowerShell" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Management-PowerShell" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Microsoft-Hyper-V-Management-PowerShell
-
name: Telnet Client feature
code: dism /Online /Disable-Feature /FeatureName:"TelnetClient" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"TelnetClient" /NoRestart
docs: https://social.technet.microsoft.com/wiki/contents/articles/38433.windows-10-enabling-telnet-client.aspx
call:
function: DisableFeature
parameters:
featureName: TelnetClient
-
name: Net.TCP Port Sharing feature
code: dism /Online /Disable-Feature /FeatureName:"WCF-TCP-PortSharing45" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"WCF-TCP-PortSharing45" /NoRestart
docs: https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/net-tcp-port-sharing
call:
function: DisableFeature
parameters:
featureName: WCF-TCP-PortSharing45
-
name: SMB Direct feature
code: dism /Online /Disable-Feature /FeatureName:"SmbDirect" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"SmbDirect" /NoRestart
docs: https://docs.microsoft.com/en-us/windows-server/storage/file-server/smb-direct
call:
function: DisableFeature
parameters:
featureName: SmbDirect
-
name: TFTP Client feature
code: dism /Online /Disable-Feature /FeatureName:"TFTP" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"TFTP" /NoRestart
call:
function: DisableFeature
parameters:
featureName: TFTP
-
category: Printing features
children:
@@ -3841,60 +3866,86 @@ actions:
children:
-
name: Internet Printing Client
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-InternetPrinting-Client" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-InternetPrinting-Client" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Printing-Foundation-InternetPrinting-Client
-
name: LPD Print Service
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-LPDPrintService" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-LPDPrintService" /NoRestart
call:
function: DisableFeature
parameters:
featureName: LPDPrintService
-
name: LPR Port Monitor feature
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-LPRPortMonitor" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-LPRPortMonitor" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Printing-Foundation-LPRPortMonitor
-
name: Microsoft Print to PDF feature
code: dism /Online /Disable-Feature /FeatureName:"Printing-PrintToPDFServices-Features" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-PrintToPDFServices-Features" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Printing-PrintToPDFServices-Features
-
name: Print and Document Services feature
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-Features" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-Features" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Printing-Foundation-Features
-
name: Work Folders Client feature
code: dism /Online /Disable-Feature /FeatureName:"WorkFolders-Client" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"WorkFolders-Client" /NoRestart
docs: https://docs.microsoft.com/en-us/windows-server/storage/work-folders/work-folders-overview
call:
function: DisableFeature
parameters:
featureName: WorkFolders-Client
-
category: XPS support
children:
-
name: XPS Services feature
code: dism /Online /Disable-Feature /FeatureName:"Printing-XPSServices-Features" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-XPSServices-Features" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Printing-XPSServices-Features
-
name: XPS Viewer feature
code: dism /Online /Disable-Feature /FeatureName:"Xps-Foundation-Xps-Viewer" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"Xps-Foundation-Xps-Viewer" /NoRestart
call:
function: DisableFeature
parameters:
featureName: Xps-Foundation-Xps-Viewer
-
name: Media Features feature
code: dism /Online /Disable-Feature /FeatureName:"MediaPlayback" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"MediaPlayback" /NoRestart
call:
function: DisableFeature
parameters:
featureName: MediaPlayback
-
name: Scan Management feature
code: dism /Online /Disable-Feature /FeatureName:"ScanManagementConsole" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"ScanManagementConsole" /NoRestart
call:
function: DisableFeature
parameters:
featureName: ScanManagementConsole
-
name: Windows Fax and Scan feature
code: dism /Online /Disable-Feature /FeatureName:"FaxServicesClientPackage" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"FaxServicesClientPackage" /NoRestart
call:
function: DisableFeature
parameters:
featureName: FaxServicesClientPackage
-
name: Windows Media Player feature
code: dism /Online /Disable-Feature /FeatureName:"WindowsMediaPlayer" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"WindowsMediaPlayer" /NoRestart
call:
function: DisableFeature
parameters:
featureName: WindowsMediaPlayer
-
name: Windows Search feature
code: dism /Online /Disable-Feature /FeatureName:"SearchEngine-Client-Package" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"SearchEngine-Client-Package" /NoRestart
call:
function: DisableFeature
parameters:
featureName: SearchEngine-Client-Package
-
category: Uninstall capabilities & features on demand
docs: https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/features-on-demand-non-language-fod#fods-that-are-not-preinstalled-but-may-need-to-be-preinstalled
@@ -4298,6 +4349,11 @@ functions:
# https://docs.microsoft.com/en-us/previous-versions/windows/desktop/xperf/image-file-execution-options
code: reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /t REG_SZ /d "%windir%\System32\taskkill.exe" /f
revertCode: reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /f
-
name: DisableFeature
parameters: [ featureName ]
code: dism /Online /Disable-Feature /FeatureName:"{{ $featureName }}" /NoRestart
revertCode: dism /Online /Enable-Feature /FeatureName:"{{ $featureName }}" /NoRestart
-
name: UninstallStoreApp
parameters: [ packageName ]

View File

@@ -47,7 +47,7 @@ function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
}
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script :\n ${duplicateLines.join('\n')}`);
throw Error(`Duplicates detected in script:\n${duplicateLines.map((line, index) => `(${index}) - ${line}`).join('\n')}`);
}
}

View File

@@ -1,30 +0,0 @@
export function throttle<T extends []>(
callback: (..._: T) => void, wait: number,
timer: ITimer = NodeTimer): (..._: T) => void {
let queuedToRun: ReturnType<typeof setTimeout>;
let previouslyRun: number;
return function invokeFn(...args: T) {
const now = timer.dateNow();
if (queuedToRun) {
queuedToRun = timer.clearTimeout(queuedToRun) as undefined;
}
if (!previouslyRun || (now - previouslyRun >= wait)) {
callback(...args);
previouslyRun = now;
} else {
queuedToRun = timer.setTimeout(invokeFn.bind(null, ...args), wait - (now - previouslyRun));
}
};
}
export interface ITimer {
setTimeout: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
clearTimeout: (timeoutId: ReturnType<typeof setTimeout>) => void;
dateNow(): number;
}
const NodeTimer: ITimer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};

View File

@@ -1,5 +1,9 @@
'use strict';
// This is main process of Electron, started as first thing when app starts.
// This script is running through entire life of the application.
// It doesn't have any windows which you can see on screen, opens the main window from here.
import { app, protocol, BrowserWindow, shell } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
@@ -34,6 +38,7 @@ function createWindow() {
width: 1350,
height: 955,
webPreferences: {
contextIsolation: false, // To reach node https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1285
// Use pluginOptions.nodeIntegration, leave this alone
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
nodeIntegration: (process.env
@@ -110,14 +115,14 @@ if (isDevelopment) {
function loadApplication(window: BrowserWindow) {
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
loadUrlWithNodeWorkaround(win, process.env.WEBPACK_DEV_SERVER_URL as string);
if (!process.env.IS_TEST) {
win.webContents.openDevTools();
}
} else {
createProtocol('app');
// Load the index.html when not in development
win.loadURL('app://./index.html');
loadUrlWithNodeWorkaround(win, 'app://./index.html');
// tslint:disable-next-line:max-line-length
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
}
@@ -131,3 +136,10 @@ function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
}
});
}
// Workaround for https://github.com/electron/electron/issues/19554 otherwise fs does not work
function loadUrlWithNodeWorkaround(window: BrowserWindow, url: string) {
setTimeout(() => {
window.loadURL(url);
}, 10);
}

View File

@@ -12,11 +12,11 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue';
import TheHeader from '@/presentation/components/TheHeader.vue';
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
@Component({
components: {

View File

@@ -17,25 +17,18 @@
v-on:click="copyCodeAsync"
icon-prefix="fas" icon-name="copy">
</IconButton>
<modal :name="macOsModalName" height="auto" :scrollable="true" :adaptive="true"
v-if="this.isMacOsCollection">
<div class="modal">
<div class="modal__content">
<MacOsInstructions :fileName="this.fileName" />
</div>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(macOsModalName)"/>
</div>
</div>
</modal>
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
<MacOsInstructions :fileName="this.fileName" />
</Dialog>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
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';
@@ -51,11 +44,10 @@ import { IApplicationContext } from '@/application/Context/IApplicationContext';
components: {
IconButton,
MacOsInstructions,
Dialog,
},
})
export default class TheCodeButtons extends StatefulVue {
public readonly macOsModalName = 'macos-instructions';
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
public canRun = false;
public hasCode = false;
@@ -70,7 +62,7 @@ export default class TheCodeButtons extends StatefulVue {
const context = await this.getCurrentContextAsync();
saveCode(this.fileName, context.state);
if (this.isMacOsCollection) {
this.$modal.show(this.macOsModalName);
(this.$refs.instructionsDialog as any).show();
}
}
public async executeCodeAsync() {
@@ -134,9 +126,6 @@ async function executeCodeAsync(context: IApplicationContext) {
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.container {
display: flex;
flex-direction: row;
@@ -145,26 +134,4 @@ async function executeCodeAsync(context: IApplicationContext) {
.container > * + * {
margin-left: 30px;
}
.modal {
font-family: $normal-font;
margin-bottom: 10px;
display: flex;
flex-direction: row;
&__content {
width: 100%;
margin: 5%;
}
&__close-button {
width: auto;
font-size: 1.5em;
margin-right:0.25em;
align-self: flex-start;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
</style>

View File

@@ -9,7 +9,7 @@
<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
@@ -17,7 +17,7 @@ import { IScript } from '@/domain/IScript';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
import Responsive from '@/presentation/Responsive.vue';
import Responsive from '@/presentation/components/Shared/Responsive.vue';
@Component({
components: {

View File

@@ -26,9 +26,9 @@
<script lang="ts">
import CardListItem from './CardListItem.vue';
import Responsive from '@/presentation/Responsive.vue';
import Responsive from '@/presentation/components/Shared/Responsive.vue';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { ICategory } from '@/domain/ICategory';
import { hasDirective } from './NonCollapsingDirective';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';

View File

@@ -33,8 +33,8 @@
<script lang="ts">
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
import ScriptsTree from '@/presentation/components/Scripts/ScriptsTree/ScriptsTree.vue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
@Component({
components: {

View File

@@ -7,7 +7,7 @@
<script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
import { NonCollapsing } from '@/presentation/components/Scripts/Cards/NonCollapsingDirective';
@Component({
directives: { NonCollapsing },

View File

@@ -50,7 +50,7 @@
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import SelectableOption from './SelectableOption.vue';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';

View File

@@ -17,7 +17,7 @@
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ApplicationFactory } from '@/application/ApplicationFactory';

View File

@@ -14,7 +14,7 @@ import { Component } from 'vue-property-decorator';
import TheOsChanger from './TheOsChanger.vue';
import TheSelector from './Selector/TheSelector.vue';
import TheGrouper from './Grouping/TheGrouper.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';

View File

@@ -16,7 +16,7 @@
<script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';

View File

@@ -15,7 +15,7 @@
<script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator';
import { IReverter } from './Reverter/IReverter';
import { StatefulVue } from '@/presentation/StatefulVue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { INode } from './INode';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { getReverter } from './Reverter/ReverterFactory';

View File

@@ -14,11 +14,11 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import TheCodeArea from '@/presentation/Code/TheCodeArea.vue';
import TheScriptsList from '@/presentation/Scripts/TheScriptsList.vue';
import TheScriptsMenu from '@/presentation/Scripts/Menu/TheScriptsMenu.vue';
import HorizontalResizeSlider from '@/presentation/Scripts/Slider/HorizontalResizeSlider.vue';
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
import TheCodeArea from '@/presentation/components/Code/TheCodeArea.vue';
import TheScriptsList from '@/presentation/components/Scripts/TheScriptsList.vue';
import TheScriptsMenu from '@/presentation/components/Scripts/Menu/TheScriptsMenu.vue';
import HorizontalResizeSlider from '@/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue';
import { Grouping } from '@/presentation/components/Scripts/Menu/Grouping/Grouping';
@Component({
components: {

View File

@@ -29,12 +29,12 @@
</template>
<script lang="ts">
import TheGrouper from '@/presentation/Scripts/Menu/Grouping/TheGrouper.vue';
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
import TheGrouper from '@/presentation/components/Scripts/Menu/Grouping/TheGrouper.vue';
import ScriptsTree from '@/presentation/components/Scripts/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/components/Scripts/Cards/CardList.vue';
import { Component, Prop } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { Grouping } from '@/presentation/components/Scripts/Menu/Grouping/Grouping';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ApplicationFactory } from '@/application/ApplicationFactory';

View File

@@ -0,0 +1,58 @@
<template>
<modal
:name="name"
:scrollable="true"
:adaptive="true"
height="auto">
<div class="dialog">
<div class="dialog__content">
<slot></slot>
</div>
<div class="dialog__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(name)"/>
</div>
</div>
</modal>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class Dialog extends Vue {
private static idCounter = 0;
public name = (++Dialog.idCounter).toString();
public show(): void {
this.$modal.show(this.name);
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.dialog {
font-family: $normal-font;
margin-bottom: 10px;
display: flex;
flex-direction: row;
&__content {
width: 100%;
margin: 5%;
}
&__close-button {
width: auto;
font-size: 1.5em;
margin-right: 0.25em;
align-self: flex-start;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
</style>

View File

@@ -6,7 +6,6 @@
<script lang="ts">
import { Component, Vue, Emit } from 'vue-property-decorator';
import ResizeObserver from 'resize-observer-polyfill';
import { throttle } from './Throttle';
@Component
@@ -16,11 +15,17 @@ export default class Responsive extends Vue {
private observer: ResizeObserver;
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
public mounted() {
public async mounted() {
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
const resizeCallback = throttle(() => this.updateSize(), 200);
this.observer = new ResizeObserver(resizeCallback);
if ('ResizeObserver' in window === false) {
const module = await import('@juggle/resize-observer');
window.ResizeObserver = module.ResizeObserver;
}
this.observer = new window.ResizeObserver(resizeCallback);
this.observer.observe(this.container);
this.fireChangeEvents();
}

View File

@@ -4,7 +4,7 @@ import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { EventSubscriptionCollection } from '../infrastructure/Events/EventSubscriptionCollection';
import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
@Component

View File

@@ -0,0 +1,52 @@
export type CallbackType = (..._: any[]) => void;
export function throttle(
callback: CallbackType, waitInMs: number,
timer: ITimer = NodeTimer): CallbackType {
const throttler = new Throttler(timer, waitInMs, callback);
return (...args: any[]) => throttler.invoke(...args);
}
export interface ITimer {
setTimeout: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
clearTimeout: (timeoutId: ReturnType<typeof setTimeout>) => void;
dateNow(): number;
}
const NodeTimer: ITimer = {
setTimeout: (callback, ms) => setTimeout(callback, ms),
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
dateNow: () => Date.now(),
};
interface IThrottler {
invoke: CallbackType;
}
class Throttler implements IThrottler {
private queuedToRun: ReturnType<typeof setTimeout>;
private previouslyRun: number;
constructor(
private readonly timer: ITimer,
private readonly waitInMs: number,
private readonly callback: CallbackType) {
if (!timer) { throw new Error('undefined timer'); }
if (!waitInMs) { throw new Error('no delay to throttle'); }
if (waitInMs < 0) { throw new Error('negative delay'); }
if (!callback) { throw new Error('undefined callback'); }
}
public invoke(...args: any[]): void {
const now = this.timer.dateNow();
if (this.queuedToRun) {
this.queuedToRun = this.timer.clearTimeout(this.queuedToRun) as undefined;
}
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
this.callback(...args);
this.previouslyRun = now;
} else {
const nextCall = () => this.invoke(...args);
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
this.queuedToRun = this.timer.setTimeout(nextCall, nextCallDelayInMs);
}
}
}

View File

@@ -31,18 +31,13 @@
</div>
<div class="footer__section__item">
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
<a @click="$modal.show(modalName)">Privacy</a>
<a @click="$refs.privacyDialog.show()">Privacy</a>
</div>
</div>
</div>
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
<div class="modal">
<PrivacyPolicy class="modal__content"/>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
</div>
</div>
</modal>
<Dialog ref="privacyDialog">
<PrivacyPolicy />
</Dialog>
</div>
</template>
@@ -50,17 +45,17 @@
import { Component, Vue } from 'vue-property-decorator';
import { Environment } from '@/application/Environment/Environment';
import PrivacyPolicy from './PrivacyPolicy.vue';
import Dialog from '@/presentation/components/Shared/Dialog.vue';
import DownloadUrlList from './DownloadUrlList.vue';
import { IApplication } from '@/domain/IApplication';
import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component({
components: {
PrivacyPolicy, DownloadUrlList,
Dialog, PrivacyPolicy, DownloadUrlList,
},
})
export default class TheFooter extends Vue {
public readonly modalName = 'privacy-policy';
public readonly isDesktop = Environment.CurrentEnvironment.isDesktop;
public version: string = '';

View File

@@ -11,8 +11,8 @@
<script lang="ts">
import { Component, Watch } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { NonCollapsing } from '@/presentation/components/Scripts/Cards/NonCollapsingDirective';
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';

View File

@@ -1,6 +1,6 @@
import Vue from 'vue';
import App from './App.vue';
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
import App from './components/App.vue';
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
new ApplicationBootstrapper()
.bootstrap(Vue);

View File

@@ -5,15 +5,15 @@ import { parseCategoryCollection } from '@/application/Parser/CategoryCollection
import { parseCategory } from '@/application/Parser/CategoryParser';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
import { mockEnumParser } from '../../stubs/EnumParserStub';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import { EnumParserStub } from '../../stubs/EnumParserStub';
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
import { RecommendationLevel } from '../../../../src/domain/RecommendationLevel';
describe('CategoryCollectionParser', () => {
describe('parseCategoryCollection', () => {
@@ -74,7 +74,8 @@ describe('CategoryCollectionParser', () => {
// arrange
const collection = new CollectionDataStub();
const information = parseProjectInformation(process.env);
const expected = parseScriptingDefinition(collection.scripting, information);
const expected = new ScriptingDefinitionParser()
.parse(collection.scripting, information);
// act
const actual = parseCategoryCollection(collection, information).scripting;
// assert
@@ -89,7 +90,8 @@ describe('CategoryCollectionParser', () => {
const expectedName = 'os';
const collection = new CollectionDataStub()
.withOs(osText);
const parserMock = mockEnumParser(expectedName, osText, expectedOs);
const parserMock = new EnumParserStub<OperatingSystem>()
.setup(expectedName, osText, expectedOs);
const info = new ProjectInformationStub();
// act
const actual = parseCategoryCollection(collection, info, parserMock);

View File

@@ -0,0 +1,134 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { Expression } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { ExpressionArguments } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('Expression', () => {
describe('ctor', () => {
describe('position', () => {
it('throws if undefined', () => {
// arrange
const expectedError = 'undefined position';
const position = undefined;
// act
const act = () => new ExpressionBuilder()
.withPosition(position)
.build();
// assert
expect(act).to.throw(expectedError);
});
it('sets as expected', () => {
// arrange
const expected = new ExpressionPosition(0, 5);
// act
const actual = new ExpressionBuilder()
.withPosition(expected)
.build();
// assert
expect(actual.position).to.equal(expected);
});
});
describe('parameters', () => {
it('defaults to empty array if undefined', () => {
// arrange
const parameters = undefined;
// act
const actual = new ExpressionBuilder()
.withParameters(parameters)
.build();
// assert
expect(actual.parameters).to.have.lengthOf(0);
});
it('sets as expected', () => {
// arrange
const expected = [ 'firstParameterName', 'secondParameterName' ];
// act
const actual = new ExpressionBuilder()
.withParameters(expected)
.build();
// assert
expect(actual.parameters).to.deep.equal(expected);
});
});
describe('evaluator', () => {
it('throws if undefined', () => {
// arrange
const expectedError = 'undefined evaluator';
const evaluator = undefined;
// act
const act = () => new ExpressionBuilder()
.withEvaluator(evaluator)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('evaluate', () => {
it('returns result from evaluator', () => {
// arrange
const evaluatorMock: ExpressionEvaluator = (args) => JSON.stringify(args);
const givenArguments = { parameter1: 'value1', parameter2: 'value2' };
const expected = evaluatorMock(givenArguments);
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters(Object.keys(givenArguments))
.build();
// arrange
const actual = sut.evaluate(givenArguments);
// assert
expect(expected).to.equal(actual);
});
it('filters unused arguments', () => {
// arrange
let actual: ExpressionArguments = {};
const evaluatorMock: ExpressionEvaluator = (providedArgs) => {
Object.keys(providedArgs)
.forEach((name) => actual = {...actual, [name]: providedArgs[name] });
return '';
};
const parameterNameToHave = 'parameterToHave';
const parameterNameToIgnore = 'parameterToIgnore';
const sut = new ExpressionBuilder()
.withEvaluator(evaluatorMock)
.withParameters([ parameterNameToHave ])
.build();
const args: ExpressionArguments = {
[parameterNameToHave]: 'value-to-have',
[parameterNameToIgnore]: 'value-to-ignore',
};
const expected: ExpressionArguments = {
[parameterNameToHave]: args[parameterNameToHave],
};
// arrange
sut.evaluate(args);
// assert
expect(expected).to.deep.equal(actual);
});
});
});
class ExpressionBuilder {
private position: ExpressionPosition = new ExpressionPosition(0, 5);
private parameters: readonly string[] = new Array<string>();
public withPosition(position: ExpressionPosition) {
this.position = position;
return this;
}
public withEvaluator(evaluator: ExpressionEvaluator) {
this.evaluator = evaluator;
return this;
}
public withParameters(parameters: string[]) {
this.parameters = parameters;
return this;
}
public build() {
return new Expression(this.position, this.evaluator, this.parameters);
}
private evaluator: ExpressionEvaluator = () => '';
}

View File

@@ -0,0 +1,34 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
describe('ExpressionPosition', () => {
describe('ctor', () => {
it('sets as expected', () => {
// arrange
const expectedStart = 0;
const expectedEnd = 5;
// act
const sut = new ExpressionPosition(expectedStart, expectedEnd);
// assert
expect(sut.start).to.equal(expectedStart);
expect(sut.end).to.equal(expectedEnd);
});
describe('throws when invalid', () => {
// arrange
const testCases = [
{ start: 5, end: 5, error: 'no length (start = end = 5)' },
{ start: 5, end: 3, error: 'start (5) after end (3)' },
{ start: -1, end: 3, error: 'negative start position: -1' },
];
for (const testCase of testCases) {
it(testCase.error, () => {
// act
const act = () => new ExpressionPosition(testCase.start, testCase.end);
// assert
expect(act).to.throw(testCase.error);
});
}
});
});
});

View File

@@ -1,79 +1,127 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { ExpressionStub } from '../../../../../stubs/ExpressionStub';
import { ExpressionParserStub } from '../../../../../stubs/ExpressionParserStub';
describe('ExpressionsCompiler', () => {
describe('parameter substitution', () => {
describe('substitutes as expected', () => {
describe('compileExpressions', () => {
describe('combines expressions as expected', () => {
// arrange
const testCases = [ {
name: 'with different parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
const code = 'part1 {{ a }} part2 {{ b }} part3';
const testCases = [
{
name: 'with ordered expressions',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
],
expected: 'part1 a part2 b part3',
},
expected: 'Hello world!',
}, {
name: 'with single parameter',
code: '{{ $parameter }}!',
parameters: {
parameter: 'Hodor',
{
name: 'unordered expressions',
expressions: [
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
],
expected: 'part1 a part2 b part3',
},
expected: 'Hodor!',
}];
{
name: 'with no expressions',
expressions: [],
expected: code,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new MockableExpressionsCompiler();
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
const actual = sut.compileExpressions(code);
// assert
expect(actual).to.equal(testCase.expected);
});
}
});
describe('throws when expected value is not provided', () => {
it('passes arguments to expressions as expected', () => {
// arrange
const expected = {
parameter1: 'value1',
parameter2: 'value2',
};
const code = 'non-important';
const expressions = [
new ExpressionStub(),
new ExpressionStub(),
];
const expressionParserMock = new ExpressionParserStub()
.withResult(expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
sut.compileExpressions(code, expected);
// assert
expect(expressions[0].callHistory).to.have.lengthOf(1);
expect(expressions[0].callHistory[0]).to.equal(expected);
expect(expressions[1].callHistory).to.have.lengthOf(1);
expect(expressions[1].callHistory[0]).to.equal(expected);
});
describe('throws when expected argument is not provided', () => {
// arrange
const noParameterTestCases = [
{
name: 'empty parameters',
code: '{{ $parameter }}!',
parameters: {},
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'undefined parameters',
code: '{{ $parameter }}!',
parameters: undefined,
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: undefined,
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'unnecessary parameter provided',
code: '{{ $parameter }}!',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {
unnecessaryParameter: 'unnecessaryValue',
},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'undefined value',
code: '{{ $parameter }}!',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter'),
],
args: {
parameter: undefined,
},
expectedError: 'parameter value(s) not provided for: "parameter"',
},
{
name: 'multiple values are not',
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
parameters: {},
name: 'multiple values are not provided',
expressions: [
new ExpressionStub().withParameters('parameter1'),
new ExpressionStub().withParameters('parameter2', 'parameter3'),
],
args: {},
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
},
{
name: 'some values are provided',
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
parameters: {
expressions: [
new ExpressionStub().withParameters('parameter1'),
new ExpressionStub().withParameters('parameter2', 'parameter3'),
],
args: {
parameter2: 'value',
},
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
@@ -81,19 +129,33 @@ describe('ExpressionsCompiler', () => {
];
for (const testCase of noParameterTestCases) {
it(testCase.name, () => {
const sut = new MockableExpressionsCompiler();
const code = 'non-important-code';
const expressionParserMock = new ExpressionParserStub()
.withResult(testCase.expressions);
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
const act = () => sut.compileExpressions(code, testCase.args);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
it('calls parser with expected code', () => {
// arrange
const expected = 'expected-code';
const expressionParserMock = new ExpressionParserStub();
const sut = new MockableExpressionsCompiler(expressionParserMock);
// act
sut.compileExpressions(expected);
// assert
expect(expressionParserMock.callHistory).to.have.lengthOf(1);
expect(expressionParserMock.callHistory[0]).to.equal(expected);
});
});
});
class MockableExpressionsCompiler extends ExpressionsCompiler {
constructor() {
super();
constructor(extractor: IExpressionParser) {
super(extractor);
}
}

View File

@@ -1,141 +0,0 @@
import 'mocha';
import { expect } from 'chai';
import { generateIlCode } from '@/application/Parser/Script/Compiler/Expressions/ILCode';
describe('ILCode', () => {
describe('getUniqueParameterNames', () => {
// arrange
const testCases = [
{
name: 'empty parameters: returns an empty array',
code: 'no expressions',
expected: [ ],
},
{
name: 'single parameter: returns expected for single usage',
code: '{{ $single }}',
expected: [ 'single' ],
},
{
name: 'single parameter: returns distinct values for repeating parameters',
code: '{{ $singleRepeating }}, {{ $singleRepeating }}',
expected: [ 'singleRepeating' ],
},
{
name: 'multiple parameters: returns expected for single usage of each',
code: '{{ $firstParameter }}, {{ $secondParameter }}',
expected: [ 'firstParameter', 'secondParameter' ],
},
{
name: 'multiple parameters: returns distinct values for repeating parameters',
code: '{{ $firstParameter }}, {{ $firstParameter }}, {{ $firstParameter }} {{ $secondParameter }}, {{ $secondParameter }}',
expected: [ 'firstParameter', 'secondParameter' ],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const sut = generateIlCode(testCase.code);
const actual = sut.getUniqueParameterNames();
// assert
expect(actual).to.deep.equal(testCase.expected);
});
}
});
describe('substituteParameter', () => {
describe('substitutes by ignoring white spaces inside mustaches', () => {
// arrange
const mustacheVariations = [
'Hello {{ $test }}!',
'Hello {{$test }}!',
'Hello {{ $test}}!',
'Hello {{$test}}!'];
mustacheVariations.forEach((variation) => {
it(variation, () => {
// arrange
const ilCode = generateIlCode(variation);
const expected = 'Hello world!';
// act
const actual = ilCode
.substituteParameter('test', 'world')
.compile();
// assert
expect(actual).to.deep.equal(expected);
});
});
});
describe('substitutes as expected', () => {
// arrange
const testCases = [
{
name: 'single parameter',
code: 'Hello {{ $firstParameter }}!',
expected: 'Hello world!',
parameters: {
firstParameter: 'world',
},
},
{
name: 'single parameter repeated',
code: '{{ $firstParameter }} {{ $firstParameter }}!',
expected: 'hello hello!',
parameters: {
firstParameter: 'hello',
},
},
{
name: 'multiple parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
expected: 'Hello world!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
},
},
{
name: 'multiple parameters repeated',
code: 'He{{ $firstParameter }} {{ $secondParameter }} and He{{ $firstParameter }} {{ $secondParameter }}!',
expected: 'Hello world and Hello world!',
parameters: {
firstParameter: 'llo',
secondParameter: 'world',
},
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
let ilCode = generateIlCode(testCase.code);
for (const parameterName of Object.keys(testCase.parameters)) {
const value = testCase.parameters[parameterName];
ilCode = ilCode.substituteParameter(parameterName, value);
}
const actual = ilCode.compile();
// assert
expect(actual).to.deep.equal(testCase.expected);
});
}
});
});
describe('compile', () => {
it('throws if there are expressions left', () => {
// arrange
const expectedError = 'unknown expression: "each"';
const code = '{{ each }}';
// act
const ilCode = generateIlCode(code);
const act = () => ilCode.compile();
// assert
expect(act).to.throw(expectedError);
});
it('returns code as it is if there are no expressions', () => {
// arrange
const expected = 'I should be the same!';
const ilCode = generateIlCode(expected);
// act
const actual = ilCode.compile();
// assert
expect(actual).to.equal(expected);
});
});
});

View File

@@ -0,0 +1,87 @@
import 'mocha';
import { expect } from 'chai';
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
import { ExpressionStub } from '../../../../../../stubs/ExpressionStub';
describe('CompositeExpressionParser', () => {
describe('ctor', () => {
it('throws if one of the parsers is undefined', () => {
// arrange
const expectedError = 'undefined leaf';
const parsers: readonly IExpressionParser[] = [ undefined, mockParser() ];
// act
const act = () => new CompositeExpressionParser(parsers);
// assert
expect(act).to.throw(expectedError);
});
});
describe('findExpressions', () => {
describe('returns result from parsers as expected', () => {
// arrange
const pool = [
new ExpressionStub(), new ExpressionStub(), new ExpressionStub(),
new ExpressionStub(), new ExpressionStub(),
];
const testCases = [
{
name: 'from single parsing none',
parsers: [ mockParser() ],
expected: [],
},
{
name: 'from single parsing single',
parsers: [ mockParser(pool[0]) ],
expected: [ pool[0] ],
},
{
name: 'from single parsing multiple',
parsers: [ mockParser(pool[0], pool[1]) ],
expected: [ pool[0], pool[1] ],
},
{
name: 'from multiple parsers with each parsing single',
parsers: [
mockParser(pool[0]),
mockParser(pool[1]),
mockParser(pool[2]),
],
expected: [ pool[0], pool[1], pool[2] ],
},
{
name: 'from multiple parsers with each parsing multiple',
parsers: [
mockParser(pool[0], pool[1]),
mockParser(pool[2], pool[3], pool[4]) ],
expected: [ pool[0], pool[1], pool[2], pool[3], pool[4] ],
},
{
name: 'from multiple parsers with only some parsing',
parsers: [
mockParser(pool[0], pool[1]),
mockParser(),
mockParser(pool[2]),
mockParser(),
],
expected: [ pool[0], pool[1], pool[2] ],
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new CompositeExpressionParser(testCase.parsers);
// act
const result = sut.findExpressions('non-important-code');
// expect
expect(result).to.deep.equal(testCase.expected);
});
}
});
});
});
function mockParser(...result: IExpression[]): IExpressionParser {
return {
findExpressions: () => result,
};
}

View File

@@ -0,0 +1,122 @@
import 'mocha';
import { expect } from 'chai';
import { ExpressionEvaluator } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression';
import { IPrimitiveExpression, RegexParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/RegexParser';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
describe('RegexParser', () => {
describe('findExpressions', () => {
describe('matches regex as expected', () => {
// arrange
const testCases = [
{
name: 'returns no result when regex does not match',
regex: /hello/g,
code: 'world',
},
{
name: 'returns expected when regex matches single',
regex: /hello/g,
code: 'hello world',
},
{
name: 'returns expected when regex matches multiple',
regex: /l/g,
code: 'hello world',
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const expected = Array.from(testCase.code.matchAll(testCase.regex));
const matches = new Array<RegExpMatchArray>();
const builder = (m: RegExpMatchArray): IPrimitiveExpression => {
matches.push(m);
return mockPrimitiveExpression();
};
const sut = new RegexParserConcrete(testCase.regex, builder);
// act
const expressions = sut.findExpressions(testCase.code);
// assert
expect(expressions).to.have.lengthOf(matches.length);
expect(matches).to.deep.equal(expected);
});
}
});
it('sets evaluator as expected', () => {
// arrange
const expected = getEvaluatorStub();
const regex = /hello/g;
const code = 'hello';
const builder = (): IPrimitiveExpression => ({
evaluator: expected,
});
const sut = new RegexParserConcrete(regex, builder);
// act
const expressions = sut.findExpressions(code);
// assert
expect(expressions).to.have.lengthOf(1);
expect(expressions[0].evaluate === expected);
});
it('sets parameters as expected', () => {
// arrange
const expected = [ 'parameter1', 'parameter2' ];
const regex = /hello/g;
const code = 'hello';
const builder = (): IPrimitiveExpression => ({
evaluator: getEvaluatorStub(),
parameters: expected,
});
const sut = new RegexParserConcrete(regex, builder);
// act
const expressions = sut.findExpressions(code);
// assert
expect(expressions).to.have.lengthOf(1);
expect(expressions[0].parameters).to.equal(expected);
});
it('sets expected position', () => {
// arrange
const code = 'mate date in state is fate';
const regex = /ate/g;
const expected = [
new ExpressionPosition(1, 4),
new ExpressionPosition(6, 9),
new ExpressionPosition(15, 18),
new ExpressionPosition(23, 26),
];
const sut = new RegexParserConcrete(regex);
// act
const expressions = sut.findExpressions(code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(expected);
});
});
});
function mockBuilder(): (match: RegExpMatchArray) => IPrimitiveExpression {
return () => ({
evaluator: getEvaluatorStub(),
});
}
function getEvaluatorStub(): ExpressionEvaluator {
return () => undefined;
}
function mockPrimitiveExpression(): IPrimitiveExpression {
return {
evaluator: getEvaluatorStub(),
};
}
class RegexParserConcrete extends RegexParser {
protected regex: RegExp;
public constructor(
regex: RegExp,
private readonly builder = mockBuilder()) {
super();
this.regex = regex;
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
return this.builder(match);
}
}

View File

@@ -0,0 +1,69 @@
import 'mocha';
import { expect } from 'chai';
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionArguments } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
describe('ParameterSubstitutionParser', () => {
it('finds at expected positions', () => {
// arrange
const testCases = [ {
name: 'single parameter',
code: '{{ $parameter }}!',
expected: [ new ExpressionPosition(0, 16) ],
}, {
name: 'different parameters',
code: 'He{{ $firstParameter }} {{ $secondParameter }}!!',
expected: [ new ExpressionPosition(2, 23), new ExpressionPosition(24, 46) ],
}];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new ParameterSubstitutionParser();
// act
const expressions = sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.position);
expect(actual).to.deep.equal(testCase.expected);
});
}
});
it('evaluates as expected', () => {
const testCases = [ {
name: 'single parameter',
code: '{{ $parameter }}',
args: [ {
name: 'parameter',
value: 'Hello world',
}],
expected: [ 'Hello world' ],
},
{
name: 'different parameters',
code: '{{ $firstParameter }} {{ $secondParameter }}!',
args: [ {
name: 'firstParameter',
value: 'Hello',
},
{
name: 'firstParameter',
value: 'World',
}],
expected: [ 'Hello', 'World' ],
}];
for (const testCase of testCases) {
it(testCase.name, () => {
const sut = new ParameterSubstitutionParser();
let args: ExpressionArguments = {};
for (const arg of testCase.args) {
args = {...args, [arg.name]: arg.value };
}
// act
const expressions = sut.findExpressions(testCase.code);
// assert
const actual = expressions.map((e) => e.evaluate(args));
expect(actual).to.deep.equal(testCase.expected);
});
}
});
});

View File

@@ -46,6 +46,18 @@ describe('FunctionsCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('throws when parameters is not an array of strings', () => {
// arrange
const parameterNameWithUnexpectedType = 5;
const func = FunctionDataStub.createWithCall()
.withParameters(parameterNameWithUnexpectedType as any);
const expectedError = `unexpected parameter name type in "${func.name}"`;
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ func ]);
// assert
expect(act).to.throw(expectedError);
});
describe('throws when when function have duplicate code', () => {
it('code', () => {
// arrange

View File

@@ -10,103 +10,131 @@ import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
describe('FunctionCallCompiler', () => {
describe('compileCall', () => {
describe('call', () => {
it('throws with undefined call', () => {
// arrange
const expectedError = 'undefined call';
const call = undefined;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
const invalidCalls: readonly any[] = ['string', 33];
const sut = new MockableFunctionCallCompiler();
const functions = new SharedFunctionCollectionStub();
invalidCalls.forEach((invalidCall) => {
describe('parameter validation', () => {
describe('call', () => {
it('throws with undefined call', () => {
// arrange
const expectedError = 'undefined call';
const call = undefined;
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(invalidCall, functions);
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call is not an object', () => {
// arrange
const expectedError = 'called function(s) must be an object';
const invalidCalls: readonly any[] = ['string', 33];
const sut = new MockableFunctionCallCompiler();
const functions = new SharedFunctionCollectionStub();
invalidCalls.forEach((invalidCall) => {
// act
const act = () => sut.compileCall(invalidCall, functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if call sequence has undefined call', () => {
// arrange
const expectedError = 'undefined function call';
const call: FunctionCallData[] = [
{ function: 'function-name' },
undefined,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call sequence has undefined function name', () => {
// arrange
const expectedError = 'empty function name called';
const call: FunctionCallData[] = [
{ function: 'function-name' },
{ function: undefined },
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call parameters does not match function parameters', () => {
// arrange
const functionName = 'test-function-name';
const testCases = [
{
name: 'an unexpected parameter instead',
functionParameters: [ 'another-parameter' ],
callParameters: [ 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
{
name: 'an unexpected parameter when none required',
functionParameters: undefined,
callParameters: [ 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
{
name: 'expected and unexpected parameter',
functionParameters: [ 'expected-parameter' ],
callParameters: [ 'expected-parameter', 'unexpected-parameter' ],
expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`,
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const func = new SharedFunctionStub()
.withName('test-function-name')
.withParameters(...testCase.functionParameters);
let params: FunctionCallParametersData = {};
for (const parameter of testCase.callParameters) {
params = {...params, [parameter]: 'defined-parameter-value '};
}
const call: FunctionCallData = { function: func.name, parameters: params };
const functions = new SharedFunctionCollectionStub().withFunction(func);
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
});
describe('functions', () => {
it('throws with undefined functions', () => {
// arrange
const expectedError = 'undefined functions';
const call: FunctionCallData = { function: 'function-call' };
const functions = undefined;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call: FunctionCallData = { function: 'function-call' };
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
it('throws if call sequence has undefined call', () => {
// arrange
const expectedError = 'undefined function call';
const call: FunctionCallData[] = [
{ function: 'function-name' },
undefined,
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call sequence has undefined function name', () => {
// arrange
const expectedError = 'empty function name called';
const call: FunctionCallData[] = [
{ function: 'function-name' },
{ function: undefined },
];
const functions = new SharedFunctionCollectionStub();
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if call parameters does not match function parameters', () => {
// arrange
const unexpectedCallParameterName = 'unexpected-parameter-name';
const func = new SharedFunctionStub()
.withName('test-function-name')
.withParameters('another-parameter');
const expectedError = `function "${func.name}" has unexpected parameter(s) provided: "${unexpectedCallParameterName}"`;
const sut = new MockableFunctionCallCompiler();
const params: FunctionCallParametersData = {
[`${unexpectedCallParameterName}`]: 'unexpected-parameter-value',
};
const call: FunctionCallData = { function: func.name, parameters: params };
const functions = new SharedFunctionCollectionStub().withFunction(func);
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('functions', () => {
it('throws with undefined functions', () => {
// arrange
const expectedError = 'undefined functions';
const call: FunctionCallData = { function: 'function-call' };
const functions = undefined;
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
it('throws if function does not exist', () => {
// arrange
const expectedError = 'function does not exist';
const call: FunctionCallData = { function: 'function-call' };
const functions: ISharedFunctionCollection = {
getFunctionByName: () => { throw new Error(expectedError); },
};
const sut = new MockableFunctionCallCompiler();
// act
const act = () => sut.compileCall(call, functions);
// assert
expect(act).to.throw(expectedError);
});
});
describe('builds code as expected', () => {
describe('builds single call as expected', () => {

View File

@@ -123,9 +123,9 @@ describe('ScriptCompiler', () => {
// assert
expect(isUsed).to.equal(true);
});
it('rethrows error from ScriptCode with script name', () => {
it('rethrows error with script name', () => {
// arrange
const scriptName = 'scriptName'; // // arrange
const scriptName = 'scriptName';
const innerError = 'innerError';
const expectedError = `Script "${scriptName}" ${innerError}`;
const callCompiler: IFunctionCallCompiler = {
@@ -142,6 +142,24 @@ describe('ScriptCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('rethrows error from ScriptCode with script name', () => {
// arrange
const scriptName = 'scriptName';
const expectedError = `Script "${scriptName}" code is empty or undefined`;
const callCompiler: IFunctionCallCompiler = {
compileCall: () => ({ code: undefined, revertCode: undefined }),
};
const scriptData = ScriptDataStub.createWithCall()
.withName(scriptName);
const sut = new ScriptCompilerBuilder()
.withSomeFunctions()
.withFunctionCallCompiler(callCompiler)
.build();
// act
const act = () => sut.compile(scriptData);
// assert
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -6,7 +6,7 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
import { mockEnumParser } from '../../../stubs/EnumParserStub';
import { EnumParserStub } from '../../../stubs/EnumParserStub';
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
@@ -104,7 +104,8 @@ describe('ScriptParser', () => {
const script = ScriptDataStub.createWithCode()
.withRecommend(levelText);
const parseContext = new CategoryCollectionParseContextStub();
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
const parserMock = new EnumParserStub<RecommendationLevel>()
.setup(expectedName, levelText, expectedLevel);
// act
const actual = parseScript(script, parseContext, parserMock);
// assert

View File

@@ -8,7 +8,7 @@ import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyn
describe('SyntaxFactory', () => {
describe('getSyntax', () => {
describe('creates expected type', () => {
it('shellscript returns ShellBuilder', () => {
describe('shellscript returns ShellBuilder', () => {
// arrange
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
{ language: ScriptingLanguage.shellscript, expected: ShellScriptSyntax},

View File

@@ -0,0 +1,96 @@
import 'mocha';
import { expect } from 'chai';
import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { ProjectInformationStub } from '../../../stubs/ProjectInformationStub';
import { ExpressionsCompilerStub } from '../../../stubs/ExpressionsCompilerStub';
describe('CodeSubstituter', () => {
describe('throws with invalid parameters', () => {
// arrange
const testCases = [{
expectedError: 'undefined code',
parameters: {
code: undefined,
info: new ProjectInformationStub(),
}},
{
expectedError: 'undefined info',
parameters: {
code: 'non empty code',
info: undefined,
},
}];
for (const testCase of testCases) {
it(`throws "${testCase.expectedError}" as expected`, () => {
const sut = new CodeSubstituterBuilder().build();
// act
const act = () => sut.substitute(testCase.parameters.code, testCase.parameters.info);
// assert
expect(act).to.throw(testCase.expectedError);
});
}
});
describe('substitutes parameters as expected values', () => {
// arrange
const info = new ProjectInformationStub();
const date = new Date();
const testCases = [
{
parameter: 'homepage',
argument: info.homepage,
},
{
parameter: 'version',
argument: info.version,
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.withDate(date)
.build();
// act
sut.substitute('non empty code', info);
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].parameters[testCase.parameter]).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.build();
// act
sut.substitute(expected, new ProjectInformationStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].code).to.equal(expected);
});
});
class CodeSubstituterBuilder {
private compiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private date = new Date();
public withCompiler(compiler: IExpressionsCompiler) {
this.compiler = compiler;
return this;
}
public withDate(date: Date) {
this.date = date;
return this;
}
public build() {
return new CodeSubstituter(this.compiler, this.date);
}
}

View File

@@ -0,0 +1,110 @@
import 'mocha';
import { expect } from 'chai';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import { IEnumParser } from '@/application/Common/Enum';
import { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ProjectInformationStub } from '../../../stubs/ProjectInformationStub';
import { EnumParserStub } from '../../../stubs/EnumParserStub';
import { ScriptingDefinitionDataStub } from '../../../stubs/ScriptingDefinitionDataStub';
import { CodeSubstituterStub } from '../../../stubs/CodeSubstituterStub';
describe('ScriptingDefinitionParser', () => {
describe('parseScriptingDefinition', () => {
it('throws when info is undefined', () => {
// arrange
const info = undefined;
const definition = new ScriptingDefinitionDataStub();
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw('undefined info');
});
it('throws when definition is undefined', () => {
// arrange
const info = new ProjectInformationStub();
const definition = undefined;
const sut = new ScriptingDefinitionParserBuilder()
.build();
// act
const act = () => sut.parse(definition, info);
// assert
expect(act).to.throw('undefined definition');
});
describe('language', () => {
it('parses as expected', () => {
// arrange
const expectedLanguage = ScriptingLanguage.batchfile;
const languageText = 'batchfile';
const expectedName = 'language';
const info = new ProjectInformationStub();
const definition = new ScriptingDefinitionDataStub()
.withLanguage(languageText);
const parserMock = new EnumParserStub<ScriptingLanguage>()
.setup(expectedName, languageText, expectedLanguage);
const sut = new ScriptingDefinitionParserBuilder()
.withParser(parserMock)
.build();
// act
const actual = sut.parse(definition, info);
// assert
expect(actual.language).to.equal(expectedLanguage);
});
});
describe('substitutes code as expected', () => {
// arrange
const code = 'hello';
const expected = 'substituted';
const testCases = [
{
name: 'startCode',
getActualValue: (result: IScriptingDefinition) => result.startCode,
data: new ScriptingDefinitionDataStub()
.withStartCode(code),
},
{
name: 'endCode',
getActualValue: (result: IScriptingDefinition) => result.endCode,
data: new ScriptingDefinitionDataStub()
.withEndCode(code),
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
const info = new ProjectInformationStub();
const substituterMock = new CodeSubstituterStub()
.setup(code, info, expected);
const sut = new ScriptingDefinitionParserBuilder()
.withSubstituter(substituterMock)
.build();
// act
const definition = sut.parse(testCase.data, info);
// assert
const actual = testCase.getActualValue(definition);
expect(actual).to.equal(expected);
});
}
});
});
});
class ScriptingDefinitionParserBuilder {
private languageParser: IEnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
.setupDefaultValue(ScriptingLanguage.shellscript);
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
public withParser(parser: IEnumParser<ScriptingLanguage>) {
this.languageParser = parser;
return this;
}
public withSubstituter(substituter: ICodeSubstituter) {
this.codeSubstituter = substituter;
return this;
}
public build() {
return new ScriptingDefinitionParser(this.languageParser, this.codeSubstituter);
}
}

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