Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
448e378dc4 | ||
|
|
ac2249f256 | ||
|
|
05932c5a36 | ||
|
|
6f46cdb4ed | ||
|
|
5f527a00cf | ||
|
|
1935db1019 | ||
|
|
1f515e7be5 | ||
|
|
1a5f92021f | ||
|
|
f3c7413f52 | ||
|
|
646db90585 | ||
|
|
1f8a0cf9ab |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# 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)
|
## 0.9.2 (2021-02-13)
|
||||||
|
|
||||||
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
- Proposing new features
|
- Proposing new features
|
||||||
- Becoming a maintainer
|
- 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.
|
- Your pull requests are actively welcomed.
|
||||||
- The steps:
|
- The steps:
|
||||||
1. Fork the repo and create your branch from master.
|
1. Fork the repo and create your branch from master.
|
||||||
@@ -25,4 +25,10 @@
|
|||||||
|
|
||||||
## License
|
## 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
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- 💡 No need to run any compiled software on your computer.
|
- 💡 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.
|
- 💡 Single click to execute your script.
|
||||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
- ❗ 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.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t 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.9.2 undergroundwires/privacy.sexy:0.9.2`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.0 undergroundwires/privacy.sexy:0.10.0`
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- It's mainly responsible for
|
- It's mainly responsible for
|
||||||
- creating and event based [application state](#application-state)
|
- 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
|
## Application state
|
||||||
|
|
||||||
@@ -14,9 +14,23 @@
|
|||||||
|
|
||||||
## Application data
|
## 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
|
- 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
|
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
||||||
- Application data is defined in collection files and
|
- 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 [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.
|
- 📖 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)
|
||||||
|
|||||||
@@ -4,6 +4,20 @@
|
|||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
- 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.
|
- 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
|
## Application data
|
||||||
|
|
||||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
- 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).
|
- 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`
|
- 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)
|
- 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/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.
|
- `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.
|
- 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.
|
- 📖 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
2544
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.9.2",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -21,48 +21,49 @@
|
|||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
|
"@juggle/resize-observer": "^3.3.0",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.9.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"inversify": "^5.0.5",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"v-tooltip": "2.1.2",
|
||||||
"v-tooltip": "2.0.2",
|
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.44",
|
"@types/ace": "0.0.45",
|
||||||
"@types/chai": "^4.2.14",
|
"@types/chai": "^4.2.15",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/mocha": "^8.2.0",
|
"@types/mocha": "^8.2.1",
|
||||||
"@vue/cli-plugin-babel": "^4.5.10",
|
"@vue/cli-plugin-babel": "^4.5.11",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.9",
|
"@vue/cli-plugin-typescript": "^4.5.11",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
"@vue/cli-plugin-unit-mocha": "^4.5.11",
|
||||||
"@vue/cli-service": "^4.5.9",
|
"@vue/cli-service": "^4.5.11",
|
||||||
"@vue/test-utils": "1.1.2",
|
"@vue/test-utils": "1.1.3",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.3.3",
|
||||||
"electron": "^11.1.0",
|
"electron": "^12.0.1",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-log": "^4.3.1",
|
"electron-log": "^4.3.2",
|
||||||
"electron-updater": "^4.3.5",
|
"electron-updater": "^4.3.8",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.26.0",
|
"markdownlint-cli": "^0.27.1",
|
||||||
"remark-cli": "^9.0.0",
|
"remark-cli": "^9.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^4.0.0",
|
"remark-preset-lint-consistent": "^4.0.0",
|
||||||
"remark-validate-links": "^10.0.2",
|
"remark-validate-links": "^10.0.3",
|
||||||
"sass": "^1.30.0",
|
"sass": "^1.32.8",
|
||||||
"sass-loader": "^10.1.0",
|
"sass-loader": "^10.1.1",
|
||||||
"typescript": "^4.1.3",
|
"tslib": "^2.1.0",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
|
"typescript": "^4.2.3",
|
||||||
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import { Category } from '@/domain/Category';
|
|||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||||
validate(content);
|
validate(content);
|
||||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
const scripting = new ScriptingDefinitionParser()
|
||||||
|
.parse(content.scripting, info);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
const categories = new Array<Category>();
|
const categories = new Array<Category>();
|
||||||
for (const action of content.actions) {
|
for (const action of content.actions) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,31 +1,49 @@
|
|||||||
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
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 {
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
|
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||||
protected constructor() { }
|
|
||||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||||
let intermediateCode = generateIlCode(code);
|
const expressions = this.extractor.findExpressions(code);
|
||||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat();
|
||||||
return intermediateCode.compile();
|
const uniqueParameterNames = Array.from(new Set(requiredParameterNames));
|
||||||
|
ensureRequiredArgsProvided(uniqueParameterNames, parameters);
|
||||||
|
return compileExpressions(expressions, code, parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
|
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) {
|
||||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
let compiledCode = '';
|
||||||
ensureValuesProvided(parameterNames, parameters);
|
expressions = expressions
|
||||||
for (const parameterName of parameterNames) {
|
.slice() // copy the array to not mutate the parameter
|
||||||
const parameterValue = parameters[parameterName];
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
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) {
|
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) {
|
||||||
nameValues = nameValues || {};
|
parameters = parameters || [];
|
||||||
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
|
args = args || {};
|
||||||
if (notProvidedNames.length) {
|
if (!parameters.length) {
|
||||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
|
return;
|
||||||
|
}
|
||||||
|
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter]));
|
||||||
|
if (notProvidedParameters.length) {
|
||||||
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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('","')}"`;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
|
export interface IExpressionParser {
|
||||||
|
findExpressions(code: string): IExpression[];
|
||||||
|
}
|
||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ function ensureValidFunctions(functions: readonly FunctionData[]) {
|
|||||||
ensureNoDuplicatesInParameterNames(functions);
|
ensureNoDuplicatesInParameterNames(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
ensureNoDuplicateCode(functions);
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
ensureExpectedParameterNameTypes(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
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)}`);
|
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[]) {
|
function printNames(holders: readonly InstructionHolder[]) {
|
||||||
return printList(holders.map((holder) => holder.name));
|
return printList(holders.map((holder) => holder.name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
|
|||||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||||
protected constructor(
|
protected constructor(
|
||||||
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { }
|
||||||
public compileCall(
|
public compileCall(
|
||||||
call: ScriptFunctionCallData,
|
call: ScriptFunctionCallData,
|
||||||
functions: ISharedFunctionCollection): ICompiledCode {
|
functions: ISharedFunctionCollection): ICompiledCode {
|
||||||
@@ -32,11 +32,12 @@ export class FunctionCallCompiler implements IFunctionCallCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
const unexpectedParameters = actual.filter((callParam) => !expected.includes(callParam));
|
||||||
.filter((callParam) => !func.parameters.includes(callParam));
|
|
||||||
if (unexpectedParameters.length) {
|
if (unexpectedParameters.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
|
||||||
|
export interface ICodeSubstituter {
|
||||||
|
substitute(code: string, info: IProjectInformation): string;
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -83,41 +83,78 @@ actions:
|
|||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Clear Safari browsing history
|
name: Clear Safari browsing history
|
||||||
|
docs:
|
||||||
|
- https://discussions.apple.com/thread/7586106?answerId=30314600022#30314600022
|
||||||
|
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
|
||||||
code: |-
|
code: |-
|
||||||
rm -f ~/Library/Safari/History.plist
|
rm -f ~/Library/Safari/History.db
|
||||||
rm -f ~/Library/Safari/HistoryIndex.sk
|
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
|
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
|
code: rm -f ~/Library/Safari/Downloads.plist
|
||||||
-
|
-
|
||||||
name: Clear Safari top sites
|
name: Clear Safari top sites
|
||||||
|
docs: https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
|
||||||
code: rm -f ~/Library/Safari/TopSites.plist
|
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
|
code: rm -f ~/Library/Safari/LastSession.plist
|
||||||
-
|
-
|
||||||
name: Clear Safari caches
|
category: Clear Safari caches
|
||||||
code: |-
|
children:
|
||||||
rm -f ~/Library/Caches/com.apple.Safari/Cache.db
|
-
|
||||||
rm -f ~/Library/Safari/WebpageIcons.db
|
name: Clear Safari cached blobs, URLs and timestamps
|
||||||
rm -rf ~/Library/Caches/com.apple.Safari/Webpage Previews
|
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
|
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
|
name: Clear search history embedded in Safari preferences
|
||||||
|
docs: https://krypted.com/tag/recentsearchstrings/
|
||||||
code: defaults write ~/Library/Preferences/com.apple.Safari RecentSearchStrings '( )'
|
code: defaults write ~/Library/Preferences/com.apple.Safari RecentSearchStrings '( )'
|
||||||
-
|
-
|
||||||
name: Clear Safari cookies
|
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
|
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
|
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
|
code: rm -f ~/Library/Safari/UserNotificationPreferences.plist
|
||||||
-
|
-
|
||||||
name: Clear Safari per-site preferences for Downloads, Geolocation, PopUps, and Autoplays
|
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
|
code: rm -f ~/Library/Safari/PerSitePreferences.db
|
||||||
-
|
-
|
||||||
category: Clear Firefox history
|
category: Clear Firefox history
|
||||||
|
|||||||
@@ -3776,22 +3776,31 @@ actions:
|
|||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Direct Play feature
|
name: Direct Play feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"DirectPlay" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"DirectPlay" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: DirectPlay
|
||||||
-
|
-
|
||||||
name: Internet Explorer feature
|
name: Internet Explorer feature
|
||||||
code: |-
|
call:
|
||||||
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-x64" /NoRestart
|
-
|
||||||
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-x84" /NoRestart
|
function: DisableFeature
|
||||||
dism /Online /Disable-Feature /FeatureName:"Internet-Explorer-Optional-amd64" /NoRestart
|
parameters:
|
||||||
revertCode: |-
|
featureName: Internet-Explorer-Optional-x64
|
||||||
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-x64" /NoRestart
|
-
|
||||||
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-x84" /NoRestart
|
function: DisableFeature
|
||||||
dism /Online /Enable-Feature /FeatureName:"Internet-Explorer-Optional-amd64" /NoRestart
|
parameters:
|
||||||
|
featureName: Internet-Explorer-Optional-x84
|
||||||
|
-
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Internet-Explorer-Optional-amd64
|
||||||
-
|
-
|
||||||
name: Legacy Components feature
|
name: Legacy Components feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"LegacyComponents" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"LegacyComponents" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: LegacyComponents
|
||||||
-
|
-
|
||||||
category: Server features for developers & administrators
|
category: Server features for developers & administrators
|
||||||
children:
|
children:
|
||||||
@@ -3800,39 +3809,55 @@ actions:
|
|||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Hyper-V feature
|
name: Hyper-V feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-All" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-All" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Microsoft-Hyper-V-All
|
||||||
-
|
-
|
||||||
name: Hyper-V GUI Management Tools feature
|
name: Hyper-V GUI Management Tools feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Management-Clients" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Management-Clients" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Microsoft-Hyper-V-Management-Clients
|
||||||
-
|
-
|
||||||
name: Hyper-V Management Tools feature
|
name: Hyper-V Management Tools feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Tools-All" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Tools-All" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Microsoft-Hyper-V-Tools-All
|
||||||
-
|
-
|
||||||
name: Hyper-V Module for Windows PowerShell feature
|
name: Hyper-V Module for Windows PowerShell feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Microsoft-Hyper-V-Management-PowerShell" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Microsoft-Hyper-V-Management-PowerShell" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Microsoft-Hyper-V-Management-PowerShell
|
||||||
-
|
-
|
||||||
name: Telnet Client feature
|
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
|
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
|
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
|
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
|
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
|
docs: https://docs.microsoft.com/en-us/windows-server/storage/file-server/smb-direct
|
||||||
|
call:
|
||||||
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: SmbDirect
|
||||||
-
|
-
|
||||||
name: TFTP Client feature
|
name: TFTP Client feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"TFTP" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"TFTP" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: TFTP
|
||||||
-
|
-
|
||||||
category: Printing features
|
category: Printing features
|
||||||
children:
|
children:
|
||||||
@@ -3841,60 +3866,86 @@ actions:
|
|||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Internet Printing Client
|
name: Internet Printing Client
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-InternetPrinting-Client" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-InternetPrinting-Client" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Printing-Foundation-InternetPrinting-Client
|
||||||
-
|
-
|
||||||
name: LPD Print Service
|
name: LPD Print Service
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-LPDPrintService" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-LPDPrintService" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: LPDPrintService
|
||||||
-
|
-
|
||||||
name: LPR Port Monitor feature
|
name: LPR Port Monitor feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-LPRPortMonitor" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-LPRPortMonitor" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Printing-Foundation-LPRPortMonitor
|
||||||
-
|
-
|
||||||
name: Microsoft Print to PDF feature
|
name: Microsoft Print to PDF feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-PrintToPDFServices-Features" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-PrintToPDFServices-Features" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Printing-PrintToPDFServices-Features
|
||||||
-
|
-
|
||||||
name: Print and Document Services feature
|
name: Print and Document Services feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-Foundation-Features" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-Foundation-Features" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Printing-Foundation-Features
|
||||||
-
|
-
|
||||||
name: Work Folders Client feature
|
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
|
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
|
category: XPS support
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: XPS Services feature
|
name: XPS Services feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Printing-XPSServices-Features" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Printing-XPSServices-Features" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Printing-XPSServices-Features
|
||||||
-
|
-
|
||||||
name: XPS Viewer feature
|
name: XPS Viewer feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"Xps-Foundation-Xps-Viewer" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"Xps-Foundation-Xps-Viewer" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: Xps-Foundation-Xps-Viewer
|
||||||
-
|
-
|
||||||
name: Media Features feature
|
name: Media Features feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"MediaPlayback" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"MediaPlayback" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: MediaPlayback
|
||||||
-
|
-
|
||||||
name: Scan Management feature
|
name: Scan Management feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"ScanManagementConsole" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"ScanManagementConsole" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: ScanManagementConsole
|
||||||
-
|
-
|
||||||
name: Windows Fax and Scan feature
|
name: Windows Fax and Scan feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"FaxServicesClientPackage" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"FaxServicesClientPackage" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: FaxServicesClientPackage
|
||||||
-
|
-
|
||||||
name: Windows Media Player feature
|
name: Windows Media Player feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"WindowsMediaPlayer" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"WindowsMediaPlayer" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: WindowsMediaPlayer
|
||||||
-
|
-
|
||||||
name: Windows Search feature
|
name: Windows Search feature
|
||||||
code: dism /Online /Disable-Feature /FeatureName:"SearchEngine-Client-Package" /NoRestart
|
call:
|
||||||
revertCode: dism /Online /Enable-Feature /FeatureName:"SearchEngine-Client-Package" /NoRestart
|
function: DisableFeature
|
||||||
|
parameters:
|
||||||
|
featureName: SearchEngine-Client-Package
|
||||||
-
|
-
|
||||||
category: Uninstall capabilities & features on demand
|
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
|
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
|
# 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
|
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
|
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
|
name: UninstallStoreApp
|
||||||
parameters: [ packageName ]
|
parameters: [ packageName ]
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
|||||||
}
|
}
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
if (duplicateLines.length !== 0) {
|
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')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
'use strict';
|
'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 { app, protocol, BrowserWindow, shell } from 'electron';
|
||||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||||
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
@@ -34,6 +38,7 @@ function createWindow() {
|
|||||||
width: 1350,
|
width: 1350,
|
||||||
height: 955,
|
height: 955,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
contextIsolation: false, // To reach node https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1285
|
||||||
// Use pluginOptions.nodeIntegration, leave this alone
|
// Use pluginOptions.nodeIntegration, leave this alone
|
||||||
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
||||||
nodeIntegration: (process.env
|
nodeIntegration: (process.env
|
||||||
@@ -110,14 +115,14 @@ if (isDevelopment) {
|
|||||||
function loadApplication(window: BrowserWindow) {
|
function loadApplication(window: BrowserWindow) {
|
||||||
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||||
// Load the url of the dev server if in development mode
|
// 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) {
|
if (!process.env.IS_TEST) {
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
createProtocol('app');
|
createProtocol('app');
|
||||||
// Load the index.html when not in development
|
// 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
|
// 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
|
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);
|
||||||
|
}
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/components/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/components/TheFooter/TheFooter.vue';
|
||||||
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/components/Code/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
|
import TheScriptArea from '@/presentation/components/Scripts/TheScriptArea.vue';
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/components/TheSearchBar.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -17,25 +17,18 @@
|
|||||||
v-on:click="copyCodeAsync"
|
v-on:click="copyCodeAsync"
|
||||||
icon-prefix="fas" icon-name="copy">
|
icon-prefix="fas" icon-name="copy">
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<modal :name="macOsModalName" height="auto" :scrollable="true" :adaptive="true"
|
<Dialog v-if="this.isMacOsCollection" ref="instructionsDialog">
|
||||||
v-if="this.isMacOsCollection">
|
|
||||||
<div class="modal">
|
|
||||||
<div class="modal__content">
|
|
||||||
<MacOsInstructions :fileName="this.fileName" />
|
<MacOsInstructions :fileName="this.fileName" />
|
||||||
</div>
|
</Dialog>
|
||||||
<div class="modal__close-button">
|
|
||||||
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(macOsModalName)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
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 { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
|
import Dialog from '@/presentation/components/Shared/Dialog.vue';
|
||||||
import IconButton from './IconButton.vue';
|
import IconButton from './IconButton.vue';
|
||||||
import MacOsInstructions from './MacOsInstructions.vue';
|
import MacOsInstructions from './MacOsInstructions.vue';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
@@ -51,11 +44,10 @@ import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
|||||||
components: {
|
components: {
|
||||||
IconButton,
|
IconButton,
|
||||||
MacOsInstructions,
|
MacOsInstructions,
|
||||||
|
Dialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TheCodeButtons extends StatefulVue {
|
export default class TheCodeButtons extends StatefulVue {
|
||||||
public readonly macOsModalName = 'macos-instructions';
|
|
||||||
|
|
||||||
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||||
public canRun = false;
|
public canRun = false;
|
||||||
public hasCode = false;
|
public hasCode = false;
|
||||||
@@ -70,7 +62,7 @@ export default class TheCodeButtons extends StatefulVue {
|
|||||||
const context = await this.getCurrentContextAsync();
|
const context = await this.getCurrentContextAsync();
|
||||||
saveCode(this.fileName, context.state);
|
saveCode(this.fileName, context.state);
|
||||||
if (this.isMacOsCollection) {
|
if (this.isMacOsCollection) {
|
||||||
this.$modal.show(this.macOsModalName);
|
(this.$refs.instructionsDialog as any).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async executeCodeAsync() {
|
public async executeCodeAsync() {
|
||||||
@@ -134,9 +126,6 @@ async function executeCodeAsync(context: IApplicationContext) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
|
||||||
@import "@/presentation/styles/fonts.scss";
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -145,26 +134,4 @@ async function executeCodeAsync(context: IApplicationContext) {
|
|||||||
.container > * + * {
|
.container > * + * {
|
||||||
margin-left: 30px;
|
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>
|
</style>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop } from 'vue-property-decorator';
|
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 from 'ace-builds';
|
||||||
import 'ace-builds/webpack-resolver';
|
import 'ace-builds/webpack-resolver';
|
||||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||||
@@ -17,7 +17,7 @@ import { IScript } from '@/domain/IScript';
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||||
import Responsive from '@/presentation/Responsive.vue';
|
import Responsive from '@/presentation/components/Shared/Responsive.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CardListItem from './CardListItem.vue';
|
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 { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { hasDirective } from './NonCollapsingDirective';
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
import { NonCollapsing } from '@/presentation/components/Scripts/Cards/NonCollapsingDirective';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
directives: { NonCollapsing },
|
directives: { NonCollapsing },
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
import SelectableOption from './SelectableOption.vue';
|
import SelectableOption from './SelectableOption.vue';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
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 { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ import { Component } from 'vue-property-decorator';
|
|||||||
import TheOsChanger from './TheOsChanger.vue';
|
import TheOsChanger from './TheOsChanger.vue';
|
||||||
import TheSelector from './Selector/TheSelector.vue';
|
import TheSelector from './Selector/TheSelector.vue';
|
||||||
import TheGrouper from './Grouping/TheGrouper.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 { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
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 { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||||
import { IReverter } from './Reverter/IReverter';
|
import { IReverter } from './Reverter/IReverter';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
import { INode } from './INode';
|
import { INode } from './INode';
|
||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { getReverter } from './Reverter/ReverterFactory';
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import TheCodeArea from '@/presentation/Code/TheCodeArea.vue';
|
import TheCodeArea from '@/presentation/components/Code/TheCodeArea.vue';
|
||||||
import TheScriptsList from '@/presentation/Scripts/TheScriptsList.vue';
|
import TheScriptsList from '@/presentation/components/Scripts/TheScriptsList.vue';
|
||||||
import TheScriptsMenu from '@/presentation/Scripts/Menu/TheScriptsMenu.vue';
|
import TheScriptsMenu from '@/presentation/components/Scripts/Menu/TheScriptsMenu.vue';
|
||||||
import HorizontalResizeSlider from '@/presentation/Scripts/Slider/HorizontalResizeSlider.vue';
|
import HorizontalResizeSlider from '@/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue';
|
||||||
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
import { Grouping } from '@/presentation/components/Scripts/Menu/Grouping/Grouping';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -29,12 +29,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TheGrouper from '@/presentation/Scripts/Menu/Grouping/TheGrouper.vue';
|
import TheGrouper from '@/presentation/components/Scripts/Menu/Grouping/TheGrouper.vue';
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/components/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
import CardList from '@/presentation/components/Scripts/Cards/CardList.vue';
|
||||||
import { Component, Prop } from 'vue-property-decorator';
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
import { Grouping } from '@/presentation/components/Scripts/Menu/Grouping/Grouping';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
58
src/presentation/components/Shared/Dialog.vue
Normal file
58
src/presentation/components/Shared/Dialog.vue
Normal 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>
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Emit } from 'vue-property-decorator';
|
import { Component, Vue, Emit } from 'vue-property-decorator';
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
|
||||||
import { throttle } from './Throttle';
|
import { throttle } from './Throttle';
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -16,11 +15,17 @@ export default class Responsive extends Vue {
|
|||||||
private observer: ResizeObserver;
|
private observer: ResizeObserver;
|
||||||
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
|
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
|
||||||
|
|
||||||
public mounted() {
|
public async mounted() {
|
||||||
this.width = this.container.offsetWidth;
|
this.width = this.container.offsetWidth;
|
||||||
this.height = this.container.offsetHeight;
|
this.height = this.container.offsetHeight;
|
||||||
const resizeCallback = throttle(() => this.updateSize(), 200);
|
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.observer.observe(this.container);
|
||||||
this.fireChangeEvents();
|
this.fireChangeEvents();
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
|||||||
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
|
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
|
||||||
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
|
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
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
|
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
|
||||||
@Component
|
@Component
|
||||||
52
src/presentation/components/Shared/Throttle.ts
Normal file
52
src/presentation/components/Shared/Throttle.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,18 +31,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="footer__section__item">
|
<div class="footer__section__item">
|
||||||
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
|
<Dialog ref="privacyDialog">
|
||||||
<div class="modal">
|
<PrivacyPolicy />
|
||||||
<PrivacyPolicy class="modal__content"/>
|
</Dialog>
|
||||||
<div class="modal__close-button">
|
|
||||||
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,17 +45,17 @@
|
|||||||
import { Component, Vue } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { Environment } from '@/application/Environment/Environment';
|
import { Environment } from '@/application/Environment/Environment';
|
||||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||||
|
import Dialog from '@/presentation/components/Shared/Dialog.vue';
|
||||||
import DownloadUrlList from './DownloadUrlList.vue';
|
import DownloadUrlList from './DownloadUrlList.vue';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
PrivacyPolicy, DownloadUrlList,
|
Dialog, PrivacyPolicy, DownloadUrlList,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class TheFooter extends Vue {
|
export default class TheFooter extends Vue {
|
||||||
public readonly modalName = 'privacy-policy';
|
|
||||||
public readonly isDesktop = Environment.CurrentEnvironment.isDesktop;
|
public readonly isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||||
|
|
||||||
public version: string = '';
|
public version: string = '';
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Watch } from 'vue-property-decorator';
|
import { Component, Watch } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
|
||||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
import { NonCollapsing } from '@/presentation/components/Scripts/Cards/NonCollapsingDirective';
|
||||||
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './App.vue';
|
import App from './components/App.vue';
|
||||||
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './bootstrapping/ApplicationBootstrapper';
|
||||||
|
|
||||||
new ApplicationBootstrapper()
|
new ApplicationBootstrapper()
|
||||||
.bootstrap(Vue);
|
.bootstrap(Vue);
|
||||||
@@ -5,15 +5,15 @@ import { parseCategoryCollection } from '@/application/Parser/CategoryCollection
|
|||||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinitionParser';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import { mockEnumParser } from '../../stubs/EnumParserStub';
|
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
import { EnumParserStub } from '../../stubs/EnumParserStub';
|
||||||
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
|
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
|
||||||
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
|
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
|
||||||
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
||||||
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
|
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
|
||||||
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
||||||
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
|
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
|
||||||
import { RecommendationLevel } from '../../../../src/domain/RecommendationLevel';
|
|
||||||
|
|
||||||
describe('CategoryCollectionParser', () => {
|
describe('CategoryCollectionParser', () => {
|
||||||
describe('parseCategoryCollection', () => {
|
describe('parseCategoryCollection', () => {
|
||||||
@@ -74,7 +74,8 @@ describe('CategoryCollectionParser', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const collection = new CollectionDataStub();
|
const collection = new CollectionDataStub();
|
||||||
const information = parseProjectInformation(process.env);
|
const information = parseProjectInformation(process.env);
|
||||||
const expected = parseScriptingDefinition(collection.scripting, information);
|
const expected = new ScriptingDefinitionParser()
|
||||||
|
.parse(collection.scripting, information);
|
||||||
// act
|
// act
|
||||||
const actual = parseCategoryCollection(collection, information).scripting;
|
const actual = parseCategoryCollection(collection, information).scripting;
|
||||||
// assert
|
// assert
|
||||||
@@ -89,7 +90,8 @@ describe('CategoryCollectionParser', () => {
|
|||||||
const expectedName = 'os';
|
const expectedName = 'os';
|
||||||
const collection = new CollectionDataStub()
|
const collection = new CollectionDataStub()
|
||||||
.withOs(osText);
|
.withOs(osText);
|
||||||
const parserMock = mockEnumParser(expectedName, osText, expectedOs);
|
const parserMock = new EnumParserStub<OperatingSystem>()
|
||||||
|
.setup(expectedName, osText, expectedOs);
|
||||||
const info = new ProjectInformationStub();
|
const info = new ProjectInformationStub();
|
||||||
// act
|
// act
|
||||||
const actual = parseCategoryCollection(collection, info, parserMock);
|
const actual = parseCategoryCollection(collection, info, parserMock);
|
||||||
|
|||||||
@@ -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 = () => '';
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,79 +1,127 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
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('ExpressionsCompiler', () => {
|
||||||
describe('parameter substitution', () => {
|
describe('compileExpressions', () => {
|
||||||
describe('substitutes as expected', () => {
|
describe('combines expressions as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases = [ {
|
const code = 'part1 {{ a }} part2 {{ b }} part3';
|
||||||
name: 'with different parameters',
|
const testCases = [
|
||||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
{
|
||||||
parameters: {
|
name: 'with ordered expressions',
|
||||||
firstParameter: 'llo',
|
expressions: [
|
||||||
secondParameter: 'world',
|
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
|
||||||
|
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
|
||||||
|
],
|
||||||
|
expected: 'part1 a part2 b part3',
|
||||||
},
|
},
|
||||||
expected: 'Hello world!',
|
{
|
||||||
}, {
|
name: 'unordered expressions',
|
||||||
name: 'with single parameter',
|
expressions: [
|
||||||
code: '{{ $parameter }}!',
|
new ExpressionStub().withPosition(6, 13).withEvaluatedResult('a'),
|
||||||
parameters: {
|
new ExpressionStub().withPosition(20, 27).withEvaluatedResult('b'),
|
||||||
parameter: 'Hodor',
|
],
|
||||||
|
expected: 'part1 a part2 b part3',
|
||||||
},
|
},
|
||||||
expected: 'Hodor!',
|
{
|
||||||
|
name: 'with no expressions',
|
||||||
}];
|
expressions: [],
|
||||||
|
expected: code,
|
||||||
|
},
|
||||||
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
const sut = new MockableExpressionsCompiler();
|
const expressionParserMock = new ExpressionParserStub()
|
||||||
|
.withResult(testCase.expressions);
|
||||||
|
const sut = new MockableExpressionsCompiler(expressionParserMock);
|
||||||
// act
|
// act
|
||||||
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
|
const actual = sut.compileExpressions(code);
|
||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(testCase.expected);
|
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
|
// arrange
|
||||||
const noParameterTestCases = [
|
const noParameterTestCases = [
|
||||||
{
|
{
|
||||||
name: 'empty parameters',
|
name: 'empty parameters',
|
||||||
code: '{{ $parameter }}!',
|
expressions: [
|
||||||
parameters: {},
|
new ExpressionStub().withParameters('parameter'),
|
||||||
|
],
|
||||||
|
args: {},
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'undefined parameters',
|
name: 'undefined parameters',
|
||||||
code: '{{ $parameter }}!',
|
expressions: [
|
||||||
parameters: undefined,
|
new ExpressionStub().withParameters('parameter'),
|
||||||
|
],
|
||||||
|
args: undefined,
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'unnecessary parameter provided',
|
name: 'unnecessary parameter provided',
|
||||||
code: '{{ $parameter }}!',
|
expressions: [
|
||||||
parameters: {
|
new ExpressionStub().withParameters('parameter'),
|
||||||
|
],
|
||||||
|
args: {
|
||||||
unnecessaryParameter: 'unnecessaryValue',
|
unnecessaryParameter: 'unnecessaryValue',
|
||||||
},
|
},
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'undefined value',
|
name: 'undefined value',
|
||||||
code: '{{ $parameter }}!',
|
expressions: [
|
||||||
parameters: {
|
new ExpressionStub().withParameters('parameter'),
|
||||||
|
],
|
||||||
|
args: {
|
||||||
parameter: undefined,
|
parameter: undefined,
|
||||||
},
|
},
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'multiple values are not',
|
name: 'multiple values are not provided',
|
||||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
expressions: [
|
||||||
parameters: {},
|
new ExpressionStub().withParameters('parameter1'),
|
||||||
|
new ExpressionStub().withParameters('parameter2', 'parameter3'),
|
||||||
|
],
|
||||||
|
args: {},
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'some values are provided',
|
name: 'some values are provided',
|
||||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
expressions: [
|
||||||
parameters: {
|
new ExpressionStub().withParameters('parameter1'),
|
||||||
|
new ExpressionStub().withParameters('parameter2', 'parameter3'),
|
||||||
|
],
|
||||||
|
args: {
|
||||||
parameter2: 'value',
|
parameter2: 'value',
|
||||||
},
|
},
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
|
||||||
@@ -81,19 +129,33 @@ describe('ExpressionsCompiler', () => {
|
|||||||
];
|
];
|
||||||
for (const testCase of noParameterTestCases) {
|
for (const testCase of noParameterTestCases) {
|
||||||
it(testCase.name, () => {
|
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
|
// act
|
||||||
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
|
const act = () => sut.compileExpressions(code, testCase.args);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(testCase.expectedError);
|
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 {
|
class MockableExpressionsCompiler extends ExpressionsCompiler {
|
||||||
constructor() {
|
constructor(extractor: IExpressionParser) {
|
||||||
super();
|
super(extractor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -46,6 +46,18 @@ describe('FunctionsCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
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', () => {
|
describe('throws when when function have duplicate code', () => {
|
||||||
it('code', () => {
|
it('code', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
|||||||
|
|
||||||
describe('FunctionCallCompiler', () => {
|
describe('FunctionCallCompiler', () => {
|
||||||
describe('compileCall', () => {
|
describe('compileCall', () => {
|
||||||
|
describe('parameter validation', () => {
|
||||||
describe('call', () => {
|
describe('call', () => {
|
||||||
it('throws with undefined call', () => {
|
it('throws with undefined call', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -65,21 +66,45 @@ describe('FunctionCallCompiler', () => {
|
|||||||
});
|
});
|
||||||
it('throws if call parameters does not match function parameters', () => {
|
it('throws if call parameters does not match function parameters', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const unexpectedCallParameterName = 'unexpected-parameter-name';
|
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()
|
const func = new SharedFunctionStub()
|
||||||
.withName('test-function-name')
|
.withName('test-function-name')
|
||||||
.withParameters('another-parameter');
|
.withParameters(...testCase.functionParameters);
|
||||||
const expectedError = `function "${func.name}" has unexpected parameter(s) provided: "${unexpectedCallParameterName}"`;
|
let params: FunctionCallParametersData = {};
|
||||||
const sut = new MockableFunctionCallCompiler();
|
for (const parameter of testCase.callParameters) {
|
||||||
const params: FunctionCallParametersData = {
|
params = {...params, [parameter]: 'defined-parameter-value '};
|
||||||
[`${unexpectedCallParameterName}`]: 'unexpected-parameter-value',
|
}
|
||||||
};
|
|
||||||
const call: FunctionCallData = { function: func.name, parameters: params };
|
const call: FunctionCallData = { function: func.name, parameters: params };
|
||||||
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
const functions = new SharedFunctionCollectionStub().withFunction(func);
|
||||||
|
const sut = new MockableFunctionCallCompiler();
|
||||||
// act
|
// act
|
||||||
const act = () => sut.compileCall(call, functions);
|
const act = () => sut.compileCall(call, functions);
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(testCase.expectedError);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('functions', () => {
|
describe('functions', () => {
|
||||||
@@ -108,6 +133,9 @@ describe('FunctionCallCompiler', () => {
|
|||||||
expect(act).to.throw(expectedError);
|
expect(act).to.throw(expectedError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
describe('builds code as expected', () => {
|
describe('builds code as expected', () => {
|
||||||
describe('builds single call as expected', () => {
|
describe('builds single call as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
|||||||
@@ -123,9 +123,9 @@ describe('ScriptCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(isUsed).to.equal(true);
|
expect(isUsed).to.equal(true);
|
||||||
});
|
});
|
||||||
it('rethrows error from ScriptCode with script name', () => {
|
it('rethrows error with script name', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const scriptName = 'scriptName'; // // arrange
|
const scriptName = 'scriptName';
|
||||||
const innerError = 'innerError';
|
const innerError = 'innerError';
|
||||||
const expectedError = `Script "${scriptName}" ${innerError}`;
|
const expectedError = `Script "${scriptName}" ${innerError}`;
|
||||||
const callCompiler: IFunctionCallCompiler = {
|
const callCompiler: IFunctionCallCompiler = {
|
||||||
@@ -142,6 +142,24 @@ describe('ScriptCompiler', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
|||||||
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
|
||||||
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
|
import { ScriptCompilerStub } from '../../../stubs/ScriptCompilerStub';
|
||||||
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
|
import { ScriptDataStub } from '../../../stubs/ScriptDataStub';
|
||||||
import { mockEnumParser } from '../../../stubs/EnumParserStub';
|
import { EnumParserStub } from '../../../stubs/EnumParserStub';
|
||||||
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
|
import { ScriptCodeStub } from '../../../stubs/ScriptCodeStub';
|
||||||
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
|
import { CategoryCollectionParseContextStub } from '../../../stubs/CategoryCollectionParseContextStub';
|
||||||
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '../../../stubs/LanguageSyntaxStub';
|
||||||
@@ -104,7 +104,8 @@ describe('ScriptParser', () => {
|
|||||||
const script = ScriptDataStub.createWithCode()
|
const script = ScriptDataStub.createWithCode()
|
||||||
.withRecommend(levelText);
|
.withRecommend(levelText);
|
||||||
const parseContext = new CategoryCollectionParseContextStub();
|
const parseContext = new CategoryCollectionParseContextStub();
|
||||||
const parserMock = mockEnumParser(expectedName, levelText, expectedLevel);
|
const parserMock = new EnumParserStub<RecommendationLevel>()
|
||||||
|
.setup(expectedName, levelText, expectedLevel);
|
||||||
// act
|
// act
|
||||||
const actual = parseScript(script, parseContext, parserMock);
|
const actual = parseScript(script, parseContext, parserMock);
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { BatchFileSyntax } from '@/application/Parser/Script/Syntax/BatchFileSyn
|
|||||||
describe('SyntaxFactory', () => {
|
describe('SyntaxFactory', () => {
|
||||||
describe('getSyntax', () => {
|
describe('getSyntax', () => {
|
||||||
describe('creates expected type', () => {
|
describe('creates expected type', () => {
|
||||||
it('shellscript returns ShellBuilder', () => {
|
describe('shellscript returns ShellBuilder', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
|
const testCases: Array< { language: ScriptingLanguage, expected: any} > = [
|
||||||
{ language: ScriptingLanguage.shellscript, expected: ShellScriptSyntax},
|
{ language: ScriptingLanguage.shellscript, expected: ShellScriptSyntax},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
Reference in New Issue
Block a user