Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd41af466f | ||
|
|
970221b996 | ||
|
|
15004ff1f1 | ||
|
|
65226f3984 | ||
|
|
b0a7d0b53b | ||
|
|
ee43fd92a0 | ||
|
|
cf39e6d254 | ||
|
|
1260eea690 | ||
|
|
45a3669443 | ||
|
|
c9b91f6d8f | ||
|
|
9a6b903b92 | ||
|
|
7661575573 | ||
|
|
f1abd7682f | ||
|
|
575636e6b7 | ||
|
|
daa997b21b | ||
|
|
5934b17283 | ||
|
|
d7de420d5c | ||
|
|
df273f7f63 | ||
|
|
67b2d1c11c | ||
|
|
15353d0e25 | ||
|
|
f1e21babbf | ||
|
|
34b8822ac8 | ||
|
|
73e0520de7 | ||
|
|
fbc3b109b9 |
36
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Bug report (script bug or unexpected script behavior)
|
||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue with generated script(s).
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### OS
|
||||
|
||||
<!--
|
||||
Which OS are you using? What version of OS you were using?
|
||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
### Scripts
|
||||
|
||||
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . -->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!-- Add any other context about the problem here. -->
|
||||
52
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
Normal file
52
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: Bug report (unrelated to generated scripts)
|
||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
||||
labels: bug
|
||||
title: '[BUG]: '
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for reporting an issue.
|
||||
Please fill in as much of the template below as you're able.
|
||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
||||
-->
|
||||
|
||||
### Describe the bug
|
||||
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
### To Reproduce
|
||||
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!--
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
-->
|
||||
|
||||
### Distribution
|
||||
|
||||
<!--
|
||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||
- Web (on Desktop or mobile?)
|
||||
- Or desktop (Windows, macOS or Linux?)
|
||||
-->
|
||||
|
||||
### Additional context
|
||||
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for privacy.sexy
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for suggesting an idea to make privacy better. 🤗
|
||||
|
||||
Please fill in as much of the template below as you're able.
|
||||
-->
|
||||
|
||||
### Problem Description
|
||||
|
||||
<!-- Please add a clear and concise description of the problem you are seeking to solve with this feature request. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
### Proposed solution
|
||||
|
||||
<!-- Describe the solution you'd like in a clear and concise manner. -->
|
||||
|
||||
### Alternatives considered
|
||||
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
### Additional information
|
||||
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,5 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.2 (2021-02-13)
|
||||
|
||||
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
||||
* fix wrong path for NvTelemtry file in NVIDIA script | [34b8822](https://github.com/undergroundwires/privacy.sexy/commit/34b8822ac821acb47e483e21b57e380551bcf455)
|
||||
* refactor event handling to consume base class for lifecycling | [f1e21ba](https://github.com/undergroundwires/privacy.sexy/commit/f1e21babbfaac21903594a37e30163bfe3338279)
|
||||
* make compiler throw if a function call includes an unexpected parameter | [15353d0](https://github.com/undergroundwires/privacy.sexy/commit/15353d0e2513c89ee4ffd9d9c5e9e83ef69b96b6)
|
||||
* refactor vscode configuration scripts using functions #41 | [67b2d1c](https://github.com/undergroundwires/privacy.sexy/commit/67b2d1c11cd5b131dff93a4437db79d96ed8b3dc)
|
||||
* refactor state handling to make application available independent of the state | [df273f7](https://github.com/undergroundwires/privacy.sexy/commit/df273f7f635ab156ac51a8dfb3fec66c4979f1c4)
|
||||
* add test to ensure correct shared functions are being parsed | [d7de420](https://github.com/undergroundwires/privacy.sexy/commit/d7de420d5c91bd9ce64880cd4a4391ad3a0a5401)
|
||||
* refactor and add tests for NonCollapsingDirective | [5934b17](https://github.com/undergroundwires/privacy.sexy/commit/5934b1728328c3b2ece1597b74dd87477d162175)
|
||||
* add GitHub issue templates | [daa997b](https://github.com/undergroundwires/privacy.sexy/commit/daa997b21b624d133c6f5e4cd6b70214588f9144)
|
||||
* correct the typo in application.md (#60) | [575636e](https://github.com/undergroundwires/privacy.sexy/commit/575636e6b728a2bdd1a9bd72c57bbf2752f10887)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.1...0.9.2)
|
||||
|
||||
## 0.9.1 (2021-01-23)
|
||||
|
||||
* in CI/CD, allow publishing to github if release is more than 2 hours old electron-userland/electron-builder#2074 | [cf907d0](https://github.com/undergroundwires/privacy.sexy/commit/cf907d029a6d80682ba78ec887a9c4fab639db51)
|
||||
* in CI/CD, publish packages for other OSes if single one fails | [4015e2c](https://github.com/undergroundwires/privacy.sexy/commit/4015e2ccd8492e0693365b70fbfe3bd0ac7a6ea2)
|
||||
* specify desktop publish targets as defaults (may) change | [2316e3f](https://github.com/undergroundwires/privacy.sexy/commit/2316e3fb6867e5d765eafcf675b77f88bd2a0f52)
|
||||
* fix selection state indicator on cards not showing up | [8b0e47d](https://github.com/undergroundwires/privacy.sexy/commit/8b0e47da38c49cfe2645d7d25970c448ecd200f8)
|
||||
* transpile using babel for legacy browser support | [7930bef](https://github.com/undergroundwires/privacy.sexy/commit/7930bef48c4e9a4fe0823673958ed8377f5ee533)
|
||||
* fix node APIs no longer working on desktop nklayman/vue-cli-plugin-electron-builder#610, nklayman/vue-cli-plugin-electron-builder#742 | [d7f9ef1](https://github.com/undergroundwires/privacy.sexy/commit/d7f9ef1cbebe911aa19f29be8c5fa9360550793e)
|
||||
* improve explanation for selections | [229c13a](https://github.com/undergroundwires/privacy.sexy/commit/229c13a195dee92e4a31731b7b41c319273a16f1)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.0...0.9.1)
|
||||
|
||||
## 0.9.0 (2021-01-15)
|
||||
|
||||
* refactor application.yaml to become an os definition #40 | [f7557bc](https://github.com/undergroundwires/privacy.sexy/commit/f7557bcc0faf44e8395b68c7eb14c5f715f07b92)
|
||||
|
||||
@@ -23,17 +23,6 @@
|
||||
- ❗ DON'T
|
||||
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Handle the state in presentation layer
|
||||
|
||||
- There are two types of components:
|
||||
- **Stateless**, extends `Vue`
|
||||
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
||||
- The source of truth for the state lies in application layer ([`./src/application/`](src/application/)) and must be updated from the views if they're mutating the state
|
||||
- They mutate or/and react to state changes in [ApplicationContext](src/application/Context/ApplicationContext.ts).
|
||||
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
||||
|
||||
30
README.md
30
README.md
@@ -14,11 +14,13 @@
|
||||
|
||||
## Get started
|
||||
|
||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-Setup-0.9.0.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-0.9.0.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.0/privacy.sexy-0.9.0.dmg)
|
||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||
- 💡 No need to run any compiled software on your computer.
|
||||
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-Setup-0.9.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.2/privacy.sexy-0.9.2.AppImage).
|
||||
- 💡 Single click to execute your script.
|
||||
- ❗ Come back regularly to apply latest version for stronger privacy and security.
|
||||
|
||||
[](https://privacy.sexy)
|
||||
[](https://privacy.sexy)
|
||||
|
||||
## Why
|
||||
|
||||
@@ -51,29 +53,17 @@
|
||||
- Development: `npm run serve` to compile & hot-reload for development.
|
||||
- Production: `npm run build` to prepare files for distribution.
|
||||
- Or run using Docker:
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.0 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.0 undergroundwires/privacy.sexy:0.9.0`
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.2 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.2 undergroundwires/privacy.sexy:0.9.2`
|
||||
|
||||
## Architecture
|
||||
## Architecture overview
|
||||
|
||||
### Application
|
||||
|
||||
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||
- Application uses highly decoupled models & services in different DDD layers.
|
||||
- **Domain layer** is where the application is modelled with validation logic.
|
||||
- **Presentation Layer**
|
||||
- Consists of Vue.js components and other UI-related code.
|
||||
- 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.
|
||||
- **Application Layer**
|
||||
- Keeps the application state using [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
||||
- [ApplicationContext](src/application/Context/ApplicationContext.ts)
|
||||
- Holds the [CategoryCollectionState](src/application/Context/State/CategoryCollectionState.ts)] for each OS
|
||||
- Same instance is shared throughout the application
|
||||
- The scripts are defined and controlled in [yaml files](src/application/collections/) per OS
|
||||
- Uses [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)
|
||||
- 📖 See [extend scripts](#extend-scripts) to read about how to extend them.
|
||||
- 📖 Read more on • [Presentation](./docs/presentation.md) • [Application](./docs/application.md)
|
||||
|
||||

|
||||
|
||||
|
||||
22
docs/application.md
Normal file
22
docs/application.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Application
|
||||
|
||||
- It's mainly responsible for
|
||||
- creating and event based [application state](#application-state)
|
||||
- parsing and compiling [application data](#application-data)
|
||||
|
||||
## Application state
|
||||
|
||||
- [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) holds the [CategoryCollectionState](./../src/application/Context/State/CategoryCollectionState.ts) for each OS
|
||||
- Uses [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
||||
- Same instance is shared throughout the application to ensure consistent state
|
||||
- 📖 See [Application State | Presentation layer](./presentation.md#application-state) to read more about how the state should be managed by the presentation layer.
|
||||
- 📖 See [ApplicationContext.ts](./../src/application/Context/ApplicationContext.ts) to start diving into the state code.
|
||||
|
||||
## Application data
|
||||
|
||||
- Compiled to `Application` domain object.
|
||||
- The scripts are defined and controlled in different data files per OS
|
||||
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
||||
- Application data is defined in collection files and
|
||||
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
||||
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
||||
@@ -101,11 +101,15 @@
|
||||
### `Function`
|
||||
|
||||
- Functions allow re-usable code throughout the defined scripts.
|
||||
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||
- Functions are templates compiled by privacy.sexy and uses special [expressions](#expressions).
|
||||
- Functions can call other functions by defining `call` property instead of `code`
|
||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||
|
||||
#### Parameter substitution
|
||||
#### Expressions
|
||||
|
||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||
|
||||
##### Parameter substitution
|
||||
|
||||
A simple function example
|
||||
|
||||
@@ -125,6 +129,22 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
argument: World
|
||||
```
|
||||
|
||||
A function can call other functions such as:
|
||||
|
||||
```yaml
|
||||
-
|
||||
function: CallerFunction
|
||||
parameters: [ 'value' ]
|
||||
call:
|
||||
function: EchoArgument
|
||||
parameters:
|
||||
argument: {{ $value }}
|
||||
-
|
||||
function: EchoArgument
|
||||
parameters: [ 'argument' ]
|
||||
code: Hello {{ $argument }} !
|
||||
```
|
||||
|
||||
#### `Function` syntax
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
@@ -135,15 +155,20 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
- `parameters`: `[` *`string`* `, ... ]`
|
||||
- Name of the parameters that the function has.
|
||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
||||
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
||||
- Parameter names must be defined to be used in [expressions](#expressions)
|
||||
- ❗ Parameter names must be unique
|
||||
`code`: *`string`* (**required**)
|
||||
`code`: *`string`* (**required** if `call` is undefined)
|
||||
- Batch file commands that will be executed
|
||||
- 💡 If defined, best practice to also define `revertCode`
|
||||
- ❗ If not defined `call` must be defined
|
||||
- `revertCode`: *`string`*
|
||||
- Code that'll undo the change done by `code` property.
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||
- A shared function or sequence of functions to call (called in order)
|
||||
- The parameter values that are sent can use [expressions](#expressions)
|
||||
- ❗ If not defined `code` must be defined
|
||||
|
||||
### `ScriptingDefinition`
|
||||
|
||||
|
||||
24
docs/presentation.md
Normal file
24
docs/presentation.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Presentation layer
|
||||
|
||||
- Consists of Vue.js components and other UI-related code.
|
||||
- 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.
|
||||
|
||||
## Application data
|
||||
|
||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
||||
- [Application.ts](../src/domain/Application.ts) domain model is the stateless application representation including
|
||||
- available scripts, collections as defined in [collection files](./collection-files.md)
|
||||
- package information as defined in [`package.json`](./../package.json)
|
||||
- 📖 See [Application data | Application layer](./presentation.md#application-data) where application data is parsed and compiled.
|
||||
|
||||
## Application state
|
||||
|
||||
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
||||
- Stateless components that does not handle state extends `Vue`
|
||||
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
||||
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
||||
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
||||
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
||||
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/StatefulVue.ts) makes lifecycling easier
|
||||
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
||||
29
docs/tests.md
Normal file
29
docs/tests.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Unit tests
|
||||
|
||||
- Unit tests are defined in [`./tests`](./../tests)
|
||||
- They follow same folder structure as [`./src`](./../src)
|
||||
|
||||
## Naming
|
||||
|
||||
- Each test suite first describe the system under test
|
||||
- E.g. tests for class `Application` is categorized under `Application`
|
||||
- Tests for specific methods are categorized under method name (if applicable)
|
||||
- E.g. test for `run()` is categorized under `run`
|
||||
|
||||
## Act, arrange, assert
|
||||
|
||||
- Tests use act, arrange and assert (AAA) pattern when applicable
|
||||
- **Arrange**
|
||||
- Should set up the test case
|
||||
- Starts with comment line `// arrange`
|
||||
- **Act**
|
||||
- Should cover the main thing to be tested
|
||||
- Starts with comment line `// act`
|
||||
- **Assert**
|
||||
- Should elicit some sort of response
|
||||
- Starts with comment line `// assert`
|
||||
|
||||
## Stubs
|
||||
|
||||
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
|
||||
- They implement dummy behavior to be functional
|
||||
@@ -1 +1 @@
|
||||
<mxfile host="www.draw.io" modified="2019-12-27T03:04:27.829Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36" etag="O-1eaon4mqUmgvki0auB" version="12.4.3" pages="1"><diagram id="rhL8jzEM8kVVyiS98U7u" name="Page-1">3Zpdk6I4FIZ/jZdakPDlpa3tzlTNbnVVV83OzE1XhACZAUKF2Or8+k0wCBhau1f8GrpayUkC5HlzTg7IAE7T9V8M5fHfNMDJABjBegBnAwA8YIpPadhsDS50t4aIkWBrMmvDM/mNldFQ1iUJcNFqyClNOMnbRp9mGfZ5y4YYo6t2s5Am7bPmKMKa4dlHiW79lwQ8VsOyjdr+CZMors5sGqomRVVjZShiFNBVwwQfB3DKKOXbvXQ9xYlkV3HZ9pu/Ubu7MIYz/p4OD0/efJxMXmZfs6kd/vj2Txp+HlrVxfFNNWIcCACqSBmPaUQzlDzW1gdGl1mA5WENUarbfKE0F0ZTGH9izjdKTbTkVJhiniaqFq8J/ya7j2xV+t6oma3VkcvCpipknG0anWTxe7Ou7laWqn4FZ/QXntKEsnJ8cFxuomY7cjncN4kqU0GXzMcHMFYzE7EI8wPtwE534S+YplhcqejHcII4eW1fB1IzN9q1q8UVO0rfj2gNrqL1TreWarWI96AbPFG3suuEMbRpNMgpyXjROPKTNIgGKnZC5Zsqctp77n24NTDtvQmzPX09fXbjOGFGwatGj0bsMEbAfmf4uONpaF0zfKjjvqJkqc70xHAhxirOTTNtIrRlXsWE4+cclRhWIlNoSxqSJGkwDmzsBVYXfQ8soOPIHjTj1TyrrgwzjteHxdDhqQ6O23Yf5U2res2H1coeN9Z7xzgTbehc07XMlmu907PMtmeBm3AtcCHX+n8RftwO2iZsBe2j7YHrnj/IQ+9eUsSbmlCnpgwnRQ+gxepJnifE7yNUtwKvFrpDW/51ieGUmzpCw77ddiKdFMXtdhTf3Yw1wzjoCOPu2cK4fS/Oc9P3V/AeEiSoOd2Mpoj0mxotzCAIO7mbhgvH+Ayp0XjPqca6UwGrw6nsczmVpYH+nIUMCSJLny8Z7hV46PnY97uALzzbso0zAHf3clGnA7h3ySjmaMDxK1aZzRugzY+DDrHTDTpwxwvD6GeF8PbYujpbtwMtOBdaV0PLcE4Lwqk6+F3jhR0L8EXxenqoeMw44ffIdj8uXB3uWIPLY4ZRQLLoD8B79dBg6ll8Kn9e6TfsLpDvBbALLoCWZQf9wBXJSRuuYelw7ZHZgRecC6+er4ldEhy7R7pVxMA2R+Obg2xrkAuO+KEU7R18e6C15+xm9ZTjiqjOk2X1MbOc47DMrqej52Olp02o+YDDSGhE/NsDZ+pLymXBVWPoBjfaiNFfG9rtMdMXCg1SEaNc7pK0fJ+giUTCEHyTSUKiTNi4fIizs35BC5w8yYRfTlw4W1DOaSoaJLLiAfm/olKAVjokN9GkPNmkyLfvPcg0CFWFkKylZA/qemYx5/KFiYkkAeZ+kFkj4tMsJEJaNvLFGcFcrHxIfEm7iDlz4UK0GKIsGC6Y+JQmW6Ykc+i4L1+X+GfxIpuINMUb5VXG1+t9r60Jb1/yMQPQ16771x1+WPchCugC79QHnvfyjIqiR91NcGPC6yvx/QtvHhV+ka3Ep5B1Lf7FnlgYpFk+3Sp6Uhq4+zkqBCNdbQh1tStb/2rrucT9q20cVTukLMLZsMBU/rI/d8Stwjwn5fPTYkhzTlLyu0wKenR0WN0O7MTXpbc6pLc+LL0o1m/WbX8JrV9PhI//AQ==</diagram></mxfile>
|
||||
<mxfile host="Electron" modified="2021-01-31T12:32:01.751Z" agent="5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="OTbSPW1ZOLwiPL6mt-j9" version="14.1.8" type="device"><diagram id="rhL8jzEM8kVVyiS98U7u" name="Page-1">3VtZd6JKF/01/dhZjA6PREjCXRbGFpM2L70QCYIoLsUI/PpvnwKcMD3cazrJl6wEqfHU3vvsKoz5Infm6e3KWU5ZPPGiL5IwSb/I+hdJUsUmflNBVhQoDaEo8FfBpCgS9wWDIPfKwqrZJph466OGSRxHSbA8LnTjxcJzk6MyZ7WKt8fNnuPoeNal43u1goHrRPXSx2CSTIvSlirsy++8wJ9WM4tCWTN3qsZlwXrqTOLtQZFsfJE7qzhOilfztONFhF2FS9Hv5pXaXWArb5H8Tofr+9ZNO9J+6A+Ljvr89N2aP5tflSq4JKtW7E0AQHkbr5Jp7McLJzL2pdereLOYeDSsgLt9m24cL1EoojD0kiQr2XQ2SYyiaTKPylovDZLv1P1KLe9GBzV6Wo7Mb7LqZpGssoNOdDs6rNt343dVv3WyimdeJ47iFV+f3OZfqKkDWGK6jjcr1/sJapUQnZXvJT9pJxXtCNGDCUp6br147iFSNFh5kZMEL8eSc0rl+rt2e3LxouT3T7iW3oXrHW9HrO1J/IC8yZfmjXfVVisnO2iwjINFsj4Y+Z4K0KD0TrnMzdI51ZP0/nlrSVRPBFNMv5fPbh3/QVHyu7rHgXcIV5L6m/bxeWSofCj7KMd9caJNOdP9yltj8Zg7XtSEcEzzdhok3mDpcFy2OCkcU/ocRNEBxhPVa02Uc+i3pLHcaFCPeJFUOjuzFVbBeqvES38KXlnbaB6nT5lN2/2eL1c7+/Rgv69OMxdHW268Z2qJR6n1m5klHmeW9B6pJb1Xav07h28fm7YoH5n2L9tLzebbm7zc+ixHxPcU1MWPDP/JPaSaV2vLZRS4l7DqI+OtWfezSt/nyGjwr3KEg/Li62ck/b6Lq8cuvnsYO7Rx6YyNN9/MxtXPkjwf6flK/pQHJLmWdHo8d4LLHo3G4mTyfBZ3UWjKbe8Njkbtk6Rq15NKUs4klfpWSaXUgDYXzysHiGzcZLPyLgr4c8v1XPcc4OOWqqjCGwDePDmLNs4A3vqbLtaoAe69eOXJ5hWgxT8H+tlrnAd60myPBeEyO0TrBNtmHdvmGWilt4K2WYN25S3jdZDE5eCfGl75zAb8V+Ft1a3CWCRB8hmxPfWFdwe3XQM3ma48ZxIs/P8DeN/dGsT6KX5Of165rO2OBU/2GufAFbyW0GpdBlwcTo7BFZQ6uOqVeAZe6a3grZ/X8DKY/OoZ6aNCLKniVfvDgazWQF4nTvKzI9qf42vo9H0ZFE9MQKze/XhHCP/K6euSGJ6+mXoORPHcu6lvh2H9mOUcviEiRLEfuJ8HULG+Nf1dQHdr2AOazamXG0eR5xKqH1efHw/N+lZUA289dZb0MpjzTywcQkUrh5QjLQr8BcoSeptoV9p1xl50T48UJHVZH8dJEs/RIKKKa8ed+ZyYowMXfaEJn0xbL4tPVtBBy6lunoOUqLwu49GnSUIfydAICenGnSyUq8CNF88BKF9duZhRusHe6uBC5VDHDZIuXn91FpOv4xV+U5FKh54budH88bDxwvUPaoKDUOtqWZ0pL/pk3aa/UpxyryhXap38fenl6a9vkp+ffvmP6f/qTOKxtxOB1Gr9GDjr9QXpl9TWVVP9PQlI5zUgvZkI6tv85xeB+EsRjBdb/AbFKX7wCtsyFdNbausLsS7LyhHlcuOqVT+QSO0zht9+K7Lr55HPT7bwS7Kf45XvLb6uvZg+TXDTAC83y4C/Z7v+Gi+TYB7k/ER2wZzfvUtSsS/XqK/0cUh9VfZfqH/Y3Hrj5vegr7fv2awhLwLh8cyf5n7J/CWIPS+Z8+Qe8V/RzamUteJWulm/gJzrlM58nfs7S3rKrpXxY7pxcyFw7r4Jrh6/dOWJPMlUmWXqizt3X1iobVmnnU/mbmDeTZZPd9/i+4GZWfYoMG+nkfM4iSe6ELBwKJnBteQ8Psj9eVtBm62paz4v103RCkzMfX/r+k/zaD1Gj/G8vXkamMV9R8wmj2l0P/gnmswfNmPp28wMldZIirKRlEbm7dNyfLttmwHL+uE/N0wwMLtFs6Qs9Om1unt9Z7bN0Mh6HfPlPkwXB33Vb7OH62/hLCjLEnfxsH6yi1i8+UM2zniUd9fTya3vPyFK2zawfkVkuulbubGxwijs2qbUBS7WQBC6oZtbgSBYgSJaobthoZl27SGVSyzQUqujCMweJqhPcfWZrW0se5Z17f6G5SMZ2KDtKLcGGq6mYOpMxb1gzY0MV8nKtOoKPA2fdajdKMOcVC4yiSVWpsiWbSbMZhjbxNgzxDFCHKZIYzEepylYmZAjnhx1PuJRmO6jD9aUz1LiqBsaQq8jpKjLMJZv6RjH9uWubaTd0M+tx3MxaRTTpmcPJawb8/Zlby4kWBvKZqqpu2jLtpbEUo4jxwBz2kOxWLshW4GWsUDJe3bfZ/kM8ZBmGOZkSq+j5bQ+FjJoiWL1wYO76el+jv4y4tiygaaijYR+GBu42po0sk1wxfEtr4hzoG27wIjpmkj4WzraZVgvjU9zow/TXQXrABY+8Qeeh4SJivi2TGcJHz830MYgvMCvoIBrzkcP2Fq8DeFPPDDU9WX0zxnVvcqpkHfDmQAN4DpUvYArdzv6/i02b/ttcyakPYo3NGl9W8xHvFPcWyg+QY5izQblGcadIXcFgc+n+xwP8CsWmhymLBBIu+grEB8CcEW8Ptqwck0zWgv0CDz1IepGGLt/Hs8O8BwoWyuc+Wifs9wKGdeARtio4C4j3Vvh0IfuVHALfg1gPEOsBtbaV0x9RPFluE8pZ4ABrn3oxkQe+FuKE30zWgt+gP1MfY1bU+8Tf9Ay8kDvS6RDi17nlAsjhWvtkdY7pHFIY4h1poInmluxOhrmdIUe8gXYiozj7UNrTCy4nAms0KqAssQKSQt96Jj0MiIfkICpwmwXGoWOwdPrsRJOrgz8ZCt/CLkubZYjZ0n3AmLJMJZq6W5i0Rj5CJgMEYNJmMiESY+0w/EeIUcpTkMs8sYAf8CNuAk06Arj5CbnGXy/ih/4Jx3JRR4b4hkdKtxrMi3t6S7laT7KMa+tCUUu+6RB4lXqwc8QP7h3eU739D7mIe0apPuUUUz6kDREeKo9e5T0qL3OuEYseFGXvIzy5/bVvCFNy/C3jPN2W3ohjYs1kkd0aT2hKUKLaW+gKD3ySfJpvQ//MEhjyG3kAvyD8XgE0qxI8XA/DYeUx7S2DJiCY7STXo0HOsB6uIZd1ZKXCTiBRkzu9z19BLwoj42kyEsT+qdYfLHUYIo4Ea+m0N6CeAXue1inRdjkM65dC+OAb8xtSow8PSM9ukmP+1c/ezVXqd4eCZQnaC+McoPvNSzTuIfhWuwPpF/eZkQ5grWT12jEPfj0aayUv87dpPBt4piwYrTPUR5R/iiv40T+oEHPtA/McuSpCK1k2DtoPInZ/4RYM+FAuSgSLvBZeBe45X7F84G4zfg+m1VtuYbLMu20n1/1IzzIO7G/kW/tvL+o2/Xf9ava0rqrGE777eYF5zx/4Iamfj0lbjAW4uvnZZ7Bi4wyV3gcx/WYAzGU9dcN2+7THqLynON7Vz8d5aSZ4UE8/dN4hAMcgAnXu0px4QwjHuC2679fB28LXWrJHhPe73DMPaaFXgn7Au+BdgbvPTZFjgAdySjXDB/RWbVu0jr5vQLfOq2vtFPUL+Kgmyst9/ZGcDrXM5wcLcxH5wHSqkjnLkv/Fv4Rd3WshDpW7BQr9V9j1flbWGEs7rczn2so9zOce6CxmVLstxo8XeBeWJwrNRWemvL9Jx8mHMP8aQo/ynCu86trNU9RT35J50BXIo/r2eRptNe78B/yf9pvyZ/orGls6ZxSxK3RtYxBK2OqYtCKmIr85vtOEdNTwwxadFLvtBf34fblCUroyniiyRV6hrrMnz8l+Uo9fuNJlurPoY0zz6GNP34Oxe3+v8qKTwHv/zVPNv4H</diagram></mxfile>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 89 KiB |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.2",
|
||||
"private": true,
|
||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||
"author": "undergroundwires",
|
||||
@@ -31,6 +31,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"inversify": "^5.0.5",
|
||||
"liquor-tree": "^0.2.70",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"v-tooltip": "2.0.2",
|
||||
"vue": "^2.6.12",
|
||||
"vue-class-component": "^7.2.6",
|
||||
|
||||
19
src/App.vue
19
src/App.vue
@@ -3,8 +3,7 @@
|
||||
<div class="wrapper">
|
||||
<TheHeader class="row" />
|
||||
<TheSearchBar class="row" />
|
||||
<TheScripts class="row"/>
|
||||
<TheCodeArea class="row" theme="xcode" />
|
||||
<TheScriptArea class="row" />
|
||||
<TheCodeButtons class="row code-buttons" />
|
||||
<TheFooter />
|
||||
</div>
|
||||
@@ -15,17 +14,15 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import TheHeader from '@/presentation/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||
import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
|
||||
import TheCodeButtons from '@/presentation/Code/CodeButtons/TheCodeButtons.vue';
|
||||
import TheScriptArea from '@/presentation/Scripts/TheScriptArea.vue';
|
||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
TheHeader,
|
||||
TheCodeArea,
|
||||
TheCodeButtons,
|
||||
TheScripts,
|
||||
TheScriptArea,
|
||||
TheSearchBar,
|
||||
TheFooter,
|
||||
},
|
||||
@@ -38,6 +35,7 @@ export default class App extends Vue {
|
||||
<style lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
@import "@/presentation/styles/media.scss";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -49,12 +47,10 @@ body {
|
||||
color: $slate;
|
||||
}
|
||||
|
||||
|
||||
#app {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
max-width: 1500px;
|
||||
|
||||
max-width: 1600px;
|
||||
.wrapper {
|
||||
margin: 0% 2% 0% 2%;
|
||||
background-color: white;
|
||||
@@ -62,18 +58,15 @@ body {
|
||||
padding: 2%;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
|
||||
.row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.code-buttons {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@import "@/presentation/styles/tooltip.scss";
|
||||
@import "@/presentation/styles/tree.scss";
|
||||
</style>
|
||||
|
||||
21
src/application/ApplicationFactory.ts
Normal file
21
src/application/ApplicationFactory.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { IApplicationFactory } from './IApplicationFactory';
|
||||
import { parseApplication } from './Parser/ApplicationParser';
|
||||
|
||||
export type ApplicationGetter = () => IApplication;
|
||||
const ApplicationGetter: ApplicationGetter = parseApplication;
|
||||
|
||||
export class ApplicationFactory implements IApplicationFactory {
|
||||
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
|
||||
private readonly getter: AsyncLazy<IApplication>;
|
||||
protected constructor(costlyGetter: ApplicationGetter) {
|
||||
if (!costlyGetter) {
|
||||
throw new Error('undefined getter');
|
||||
}
|
||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||
}
|
||||
public getAppAsync(): Promise<IApplication> {
|
||||
return this.getter.getValueAsync();
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
|
||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||
|
||||
export class ApplicationContext implements IApplicationContext {
|
||||
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>();
|
||||
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
|
||||
public collection: ICategoryCollection;
|
||||
public currentOs: OperatingSystem;
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
import { IApplicationFactory } from '../IApplicationFactory';
|
||||
import { ApplicationFactory } from '../ApplicationFactory';
|
||||
|
||||
export type ApplicationParserType = () => IApplication;
|
||||
const ApplicationParser: ApplicationParserType = parseApplication;
|
||||
|
||||
export function buildContext(
|
||||
parser = ApplicationParser,
|
||||
environment = Environment.CurrentEnvironment): IApplicationContext {
|
||||
const app = parser();
|
||||
export async function buildContextAsync(
|
||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
|
||||
if (!factory) { throw new Error('undefined factory'); }
|
||||
if (!environment) { throw new Error('undefined environment'); }
|
||||
const app = await factory.getAppAsync();
|
||||
const os = getInitialOs(app, environment);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export interface IApplicationContext {
|
||||
readonly app: IApplication;
|
||||
readonly state: ICategoryCollectionState;
|
||||
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
|
||||
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
|
||||
changeContext(os: OperatingSystem): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||
public readonly changed = new EventSource<ICodeChangedEvent>();
|
||||
public current: string;
|
||||
|
||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||
|
||||
@@ -5,6 +5,12 @@ export class BatchBuilder extends CodeBuilder {
|
||||
return '::';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${text}`;
|
||||
return `echo ${escapeForEcho(text)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '^&')
|
||||
.replace(/%/g, '%%');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@ export class ShellBuilder extends CodeBuilder {
|
||||
return '#';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${text}'`;
|
||||
return `echo '${escapeForEcho(text)}'`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeForEcho(text: string) {
|
||||
return text
|
||||
.replace(/'/g, '\'\\\'\'');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
export interface IApplicationCode {
|
||||
readonly changed: ISignal<ICodeChangedEvent>;
|
||||
readonly changed: IEventSource<ICodeChangedEvent>;
|
||||
readonly current: string;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
|
||||
export interface IUserFilter {
|
||||
readonly currentFilter: IFilterResult | undefined;
|
||||
readonly filtered: ISignal<IFilterResult>;
|
||||
readonly filterRemoved: ISignal<void>;
|
||||
readonly filtered: IEventSource<IFilterResult>;
|
||||
readonly filterRemoved: IEventSource<void>;
|
||||
setFilter(filter: string): void;
|
||||
removeFilter(): void;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { IScript } from '@/domain/IScript';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new Signal<IFilterResult>();
|
||||
public readonly filterRemoved = new Signal<void>();
|
||||
public readonly filtered = new EventSource<IFilterResult>();
|
||||
public readonly filterRemoved = new EventSource<void>();
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
|
||||
constructor(private collection: ICategoryCollection) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
export interface IUserSelection {
|
||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||
readonly totalSelected: number;
|
||||
areAllSelected(category: ICategory): boolean;
|
||||
|
||||
@@ -2,13 +2,13 @@ import { SelectedScript } from './SelectedScript';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
constructor(
|
||||
|
||||
5
src/application/IApplicationFactory.ts
Normal file
5
src/application/IApplicationFactory.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export interface IApplicationFactory {
|
||||
getAppAsync(): Promise<IApplication>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
|
||||
protected constructor() { }
|
||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
ensureValuesProvided(parameterNames, parameters);
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
|
||||
function ensureValuesProvided(names: string[], nameValues: ParameterValueDictionary) {
|
||||
nameValues = nameValues || {};
|
||||
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
|
||||
if (notProvidedNames.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface ParameterValueDictionary { [parameterName: string]: string; }
|
||||
|
||||
export interface IExpressionsCompiler {
|
||||
compileExpressions(code: string, parameters?: ParameterValueDictionary): string;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { FunctionData, InstructionHolder } from 'js-yaml-loader!*';
|
||||
import { SharedFunction } from './SharedFunction';
|
||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
import { IFunctionCompiler } from './IFunctionCompiler';
|
||||
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler';
|
||||
|
||||
export class FunctionCompiler implements IFunctionCompiler {
|
||||
public static readonly instance: IFunctionCompiler = new FunctionCompiler();
|
||||
protected constructor(
|
||||
private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) {
|
||||
}
|
||||
public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||
const collection = new SharedFunctionCollection();
|
||||
if (!functions || !functions.length) {
|
||||
return collection;
|
||||
}
|
||||
ensureValidFunctions(functions);
|
||||
functions
|
||||
.filter((func) => hasCode(func))
|
||||
.forEach((func) => {
|
||||
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode);
|
||||
collection.addFunction(shared);
|
||||
});
|
||||
functions
|
||||
.filter((func) => hasCall(func))
|
||||
.forEach((func) => {
|
||||
const code = this.functionCallCompiler.compileCall(func.call, collection);
|
||||
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
|
||||
collection.addFunction(shared);
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
function hasCode(data: FunctionData): boolean {
|
||||
return Boolean(data.code);
|
||||
}
|
||||
|
||||
function hasCall(data: FunctionData): boolean {
|
||||
return Boolean(data.call);
|
||||
}
|
||||
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
||||
// Ensure functions do not define both call and code
|
||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||
if (withBothCallAndCode.length) {
|
||||
throw new Error(`both "code" and "call" are defined in ${printNames(withBothCallAndCode)}`);
|
||||
}
|
||||
// Ensure functions have either code or call
|
||||
const hasEitherCodeOrCall = holders.filter((holder) => !hasCode(holder) && !hasCall(holder));
|
||||
if (hasEitherCodeOrCall.length) {
|
||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||
}
|
||||
}
|
||||
function printNames(holders: readonly InstructionHolder[]) {
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||
const functionsWithParameters = functions
|
||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||
for (const func of functionsWithParameters) {
|
||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||
if (duplicateParameterNames.length) {
|
||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions
|
||||
.map((func) => func.code)
|
||||
.filter((code) => code),
|
||||
);
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export interface IFunctionCompiler {
|
||||
compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface ISharedFunction {
|
||||
readonly name: string;
|
||||
readonly parameters?: readonly string[];
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export interface ISharedFunctionCollection {
|
||||
getFunctionByName(name: string): ISharedFunction;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
|
||||
export class SharedFunction implements ISharedFunction {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly parameters: readonly string[],
|
||||
public readonly code: string,
|
||||
public readonly revertCode: string,
|
||||
) {
|
||||
if (!name) { throw new Error('undefined function name'); }
|
||||
if (!code) { throw new Error(`undefined function ("${name}") code`); }
|
||||
this.parameters = parameters || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ISharedFunction } from './ISharedFunction';
|
||||
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||
|
||||
export class SharedFunctionCollection implements ISharedFunctionCollection {
|
||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||
|
||||
public addFunction(func: ISharedFunction): void {
|
||||
if (!func) { throw new Error('undefined function'); }
|
||||
if (this.functionsByName.has(func.name)) {
|
||||
throw new Error(`function with name ${func.name} already exists`);
|
||||
}
|
||||
this.functionsByName.set(func.name, func);
|
||||
}
|
||||
|
||||
public getFunctionByName(name: string): ISharedFunction {
|
||||
if (!name) { throw Error('undefined function name'); }
|
||||
const func = this.functionsByName.get(name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FunctionCallData, FunctionCallParametersData, FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||
import { IExpressionsCompiler } from '../Expressions/IExpressionsCompiler';
|
||||
import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
|
||||
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
|
||||
public compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
if (!functions) { throw new Error('undefined functions'); }
|
||||
if (!call) { throw new Error('undefined call'); }
|
||||
const compiledCodes = new Array<ICompiledCode>();
|
||||
const calls = getCallSequence(call);
|
||||
calls.forEach((currentCall, currentCallIndex) => {
|
||||
ensureValidCall(currentCall);
|
||||
const commonFunction = functions.getFunctionByName(currentCall.function);
|
||||
ensureExpectedParameters(commonFunction, currentCall);
|
||||
let functionCode = compileCode(commonFunction, currentCall.parameters, this.expressionsCompiler);
|
||||
if (currentCallIndex !== calls.length - 1) {
|
||||
functionCode = appendLine(functionCode);
|
||||
}
|
||||
compiledCodes.push(functionCode);
|
||||
});
|
||||
const compiledCode = merge(compiledCodes);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
||||
if (!func.parameters && !call.parameters) {
|
||||
return;
|
||||
}
|
||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
||||
.filter((callParam) => !func.parameters.includes(callParam));
|
||||
if (unexpectedParameters.length) {
|
||||
throw new Error(
|
||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||
return {
|
||||
code: codes.map((code) => code.code).join(''),
|
||||
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||
};
|
||||
}
|
||||
|
||||
function compileCode(
|
||||
func: FunctionData,
|
||||
parameters: FunctionCallParametersData,
|
||||
compiler: IExpressionsCompiler): ICompiledCode {
|
||||
return {
|
||||
code: compiler.compileExpressions(func.code, parameters),
|
||||
revertCode: compiler.compileExpressions(func.revertCode, parameters),
|
||||
};
|
||||
}
|
||||
|
||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||
if (typeof call !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
}
|
||||
if (call instanceof Array) {
|
||||
return call as FunctionCallData[];
|
||||
}
|
||||
return [ call as FunctionCallData ];
|
||||
}
|
||||
|
||||
function ensureValidCall(call: FunctionCallData) {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call`);
|
||||
}
|
||||
if (!call.function) {
|
||||
throw new Error(`empty function name called`);
|
||||
}
|
||||
}
|
||||
|
||||
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||
return {
|
||||
code: appendLineIfNotEmpty(code.code),
|
||||
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ICompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode?: string;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ScriptFunctionCallData } from 'js-yaml-loader!*';
|
||||
import { ICompiledCode } from './ICompiledCode';
|
||||
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
|
||||
|
||||
export interface IFunctionCallCompiler {
|
||||
compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode;
|
||||
}
|
||||
@@ -1,171 +1,42 @@
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
interface ICompiledCode {
|
||||
readonly code: string;
|
||||
readonly revertCode: string;
|
||||
}
|
||||
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||
import { IFunctionCallCompiler } from './FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCallCompiler } from './FunctionCall/FunctionCallCompiler';
|
||||
import { IFunctionCompiler } from './Function/IFunctionCompiler';
|
||||
import { FunctionCompiler } from './Function/FunctionCompiler';
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
private readonly functions: ISharedFunctionCollection;
|
||||
constructor(
|
||||
private readonly functions: readonly FunctionData[] | undefined,
|
||||
private syntax: ILanguageSyntax) {
|
||||
ensureValidFunctions(functions);
|
||||
functions: readonly FunctionData[] | undefined,
|
||||
private readonly syntax: ILanguageSyntax,
|
||||
functionCompiler: IFunctionCompiler = FunctionCompiler.instance,
|
||||
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||
) {
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
this.functions = functionCompiler.compileFunctions(functions);
|
||||
}
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
this.ensureCompilable(script.call);
|
||||
const compiledCodes = new Array<ICompiledCode>();
|
||||
const calls = getCallSequence(script.call);
|
||||
calls.forEach((currentCall, currentCallIndex) => {
|
||||
ensureValidCall(currentCall, script.name);
|
||||
const commonFunction = this.getFunctionByName(currentCall.function);
|
||||
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
||||
if (currentCallIndex !== calls.length - 1) {
|
||||
functionCode = appendLine(functionCode);
|
||||
}
|
||||
compiledCodes.push(functionCode);
|
||||
});
|
||||
const scriptCode = merge(compiledCodes);
|
||||
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
||||
}
|
||||
|
||||
private getFunctionByName(name: string): FunctionData {
|
||||
const func = this.functions.find((f) => f.name === name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
private ensureCompilable(call: ScriptFunctionCallData) {
|
||||
if (!this.functions || this.functions.length === 0) {
|
||||
throw new Error('cannot compile without shared functions');
|
||||
}
|
||||
if (typeof call !== 'object') {
|
||||
throw new Error('called function(s) must be an object');
|
||||
if (!script) { throw new Error('undefined script'); }
|
||||
try {
|
||||
const compiledCode = this.callCompiler.compileCall(script.call, this.functions);
|
||||
return new ScriptCode(
|
||||
compiledCode.code,
|
||||
compiledCode.revertCode,
|
||||
this.syntax);
|
||||
} catch (error) {
|
||||
throw Error(`Script "${script.name}" ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDuplicates(texts: readonly string[]): string[] {
|
||||
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||
const functionsWithParameters = functions
|
||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||
for (const func of functionsWithParameters) {
|
||||
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||
if (duplicateParameterNames.length) {
|
||||
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
}
|
||||
const duplicateRevertCodes = getDuplicates(functions
|
||||
.filter((func) => func.revertCode)
|
||||
.map((func) => func.revertCode));
|
||||
if (duplicateRevertCodes.length > 0) {
|
||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
if (!functions || functions.length === 0) {
|
||||
return;
|
||||
}
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
}
|
||||
|
||||
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||
return {
|
||||
code: appendLineIfNotEmpty(code.code),
|
||||
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||
};
|
||||
}
|
||||
|
||||
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||
return {
|
||||
code: codes.map((code) => code.code).join(''),
|
||||
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||
};
|
||||
}
|
||||
|
||||
function compileCode(func: FunctionData, parameters: FunctionCallParametersData): ICompiledCode {
|
||||
return {
|
||||
code: compileExpressions(func.code, parameters),
|
||||
revertCode: compileExpressions(func.revertCode, parameters),
|
||||
};
|
||||
}
|
||||
|
||||
function compileExpressions(code: string, parameters: FunctionCallParametersData): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParametersData): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
if (parameterNames.length && !parameters) {
|
||||
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||
}
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
|
||||
function ensureValidCall(call: FunctionCallData, scriptName: string) {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call in script "${scriptName}"`);
|
||||
}
|
||||
if (!call.function) {
|
||||
throw new Error(`empty function name called in script "${scriptName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||
if (call instanceof Array) {
|
||||
return call as FunctionCallData[];
|
||||
}
|
||||
return [ call as FunctionCallData ];
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ function parseCode(script: ScriptData, context: ICategoryCollectionParseContext)
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
||||
return new ScriptCode(script.code, script.revertCode, context.syntax);
|
||||
}
|
||||
|
||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||
|
||||
@@ -4,7 +4,7 @@ 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/ILCode';
|
||||
import { generateIlCode } from './Script/Compiler/Expressions/ILCode';
|
||||
|
||||
export function parseScriptingDefinition(
|
||||
definition: ScriptingDefinitionData,
|
||||
|
||||
31
src/application/collections/collection.yaml.d.ts
vendored
31
src/application/collections/collection.yaml.d.ts
vendored
@@ -18,30 +18,33 @@ declare module 'js-yaml-loader!*' {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
}
|
||||
|
||||
export interface FunctionData {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode?: string;
|
||||
parameters?: readonly string[];
|
||||
export interface InstructionHolder {
|
||||
readonly name: string;
|
||||
|
||||
readonly code?: string;
|
||||
readonly revertCode?: string;
|
||||
|
||||
readonly call?: ScriptFunctionCallData;
|
||||
}
|
||||
|
||||
export interface FunctionData extends InstructionHolder {
|
||||
readonly parameters?: readonly string[];
|
||||
}
|
||||
|
||||
export interface FunctionCallParametersData {
|
||||
[index: string]: string;
|
||||
readonly [index: string]: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallData {
|
||||
function: string;
|
||||
parameters?: FunctionCallParametersData;
|
||||
readonly function: string;
|
||||
readonly parameters?: FunctionCallParametersData;
|
||||
}
|
||||
|
||||
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
|
||||
export interface ScriptData extends DocumentableData {
|
||||
name: string;
|
||||
code?: string;
|
||||
revertCode?: string;
|
||||
call: ScriptFunctionCallData;
|
||||
recommend?: string;
|
||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||
readonly name: string;
|
||||
readonly recommend?: string;
|
||||
}
|
||||
|
||||
export interface ScriptingDefinitionData {
|
||||
|
||||
@@ -39,6 +39,248 @@ actions:
|
||||
sudo rm -rfv /var/spool/cups/c0*
|
||||
sudo rm -rfv /var/spool/cups/tmp/*
|
||||
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
||||
-
|
||||
name: Empty trash on all volumes
|
||||
recommend: strict
|
||||
code: |-
|
||||
# on all mounted volumes
|
||||
sudo rm -rfv /Volumes/*/.Trashes/* &>/dev/null
|
||||
# on main HDD
|
||||
sudo rm -rfv ~/.Trash/* &>/dev/null
|
||||
-
|
||||
name: Clear system cache files
|
||||
recommend: strict
|
||||
code: |-
|
||||
sudo rm -rfv /Library/Caches/* &>/dev/null
|
||||
sudo rm -rfv /System/Library/Caches/* &>/dev/null
|
||||
sudo rm -rfv ~/Library/Caches/* &>/dev/null
|
||||
-
|
||||
name: Clear system log files
|
||||
recommend: strict
|
||||
code: |-
|
||||
sudo rm -rfv /private/var/log/asl/*.asl &>/dev/null
|
||||
sudo rm -rfv /Library/Logs/DiagnosticReports/* &>/dev/null
|
||||
sudo rm -rfv /Library/Logs/Adobe/* &>/dev/null
|
||||
rm -rfv ~/Library/Containers/com.apple.mail/Data/Library/Logs/Mail/* &>/dev/null
|
||||
rm -rfv ~/Library/Logs/CoreSimulator/* &>/dev/null
|
||||
sudo rm -rfv /var/log/*
|
||||
-
|
||||
category: Clear browser history
|
||||
children:
|
||||
-
|
||||
category: Clear Google Chrome history
|
||||
children:
|
||||
-
|
||||
name: Clear Google Chrome browsing history
|
||||
code: |-
|
||||
rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/History &>/dev/null
|
||||
rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/History-journal &>/dev/null
|
||||
-
|
||||
name: Google Chrome Cache Files
|
||||
code: sudo rm -rfv ~/Library/Application\ Support/Google/Chrome/Default/Application\ Cache/* &>/dev/null
|
||||
-
|
||||
category: Clear Safari history
|
||||
children:
|
||||
-
|
||||
name: Clear Safari browsing history
|
||||
code: |-
|
||||
rm -f ~/Library/Safari/History.plist
|
||||
rm -f ~/Library/Safari/HistoryIndex.sk
|
||||
-
|
||||
name: Clear Safari downloads history
|
||||
code: rm -f ~/Library/Safari/Downloads.plist
|
||||
-
|
||||
name: Clear Safari top sites
|
||||
code: rm -f ~/Library/Safari/TopSites.plist
|
||||
-
|
||||
name: Clear Safari last session history
|
||||
code: rm -f ~/Library/Safari/LastSession.plist
|
||||
-
|
||||
name: Clear Safari caches
|
||||
code: |-
|
||||
rm -f ~/Library/Caches/com.apple.Safari/Cache.db
|
||||
rm -f ~/Library/Safari/WebpageIcons.db
|
||||
rm -rf ~/Library/Caches/com.apple.Safari/Webpage Previews
|
||||
-
|
||||
name: Clear copy of the Safari history
|
||||
code: rm -rf ~/Library/Caches/Metadata/Safari/History
|
||||
-
|
||||
name: Clear search history embedded in Safari preferences
|
||||
code: defaults write ~/Library/Preferences/com.apple.Safari RecentSearchStrings '( )'
|
||||
-
|
||||
name: Clear Safari cookies
|
||||
code: rm -f ~/Library/Cookies/Cookies.plists
|
||||
-
|
||||
name: Clear Safari zoom level preferences per site
|
||||
code: rm -f ~/Library/Safari/PerSiteZoomPreferences.plists
|
||||
-
|
||||
name: Clear URLs that are allowed to display notifications in Safari
|
||||
code: rm -f ~/Library/Safari/UserNotificationPreferences.plist
|
||||
-
|
||||
name: Clear Safari per-site preferences for Downloads, Geolocation, PopUps, and Autoplays
|
||||
code: rm -f ~/Library/Safari/PerSitePreferences.db
|
||||
-
|
||||
category: Clear Firefox history
|
||||
children:
|
||||
-
|
||||
name: Clear Firefox cache
|
||||
code: |-
|
||||
sudo rm -rf ~/Library/Caches/Mozilla/
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/netpredictions.sqlite
|
||||
-
|
||||
name: Delete Firefox form history
|
||||
code: |-
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/formhistory.sqlite
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/formhistory.dat
|
||||
-
|
||||
name: Delete Firefox site preferences
|
||||
code: rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/content-prefs.sqlite
|
||||
-
|
||||
name: Delete Firefox session restore data (loads after the browser closes or crashes)
|
||||
code: |-
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionCheckpoints.json
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore*.js*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore.bak*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/previous.js*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/recovery.js*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/recovery.bak*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/previous.bak*
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/sessionstore-backups/upgrade.js*-20*
|
||||
-
|
||||
name: Delete Firefox passwords
|
||||
docs: http://kb.mozillazine.org/Password_Manager
|
||||
code: |-
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.txt
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons2.txt
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons3.txt
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/signons.sqlite
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/logins.json
|
||||
-
|
||||
name: Delete Firefox HTML5 cookies
|
||||
code: rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/webappsstore.sqlite
|
||||
-
|
||||
name: Delete Firefox crash reports
|
||||
code: |-
|
||||
rm -rfv ~/Library/Application\ Support/Firefox/Crash\ Reports/
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/minidumps/*.dmp
|
||||
-
|
||||
name: Delete Firefox backup files
|
||||
code: |-
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/bookmarkbackups/*.json
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/bookmarkbackups/*.jsonlz4
|
||||
-
|
||||
name: Delete Firefox cookies
|
||||
code: |-
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.txt
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite-shm
|
||||
rm -fv ~/Library/Application\ Support/Firefox/Profiles/*/cookies.sqlite-wal
|
||||
rm -rfv ~/Library/Application\ Support/Firefox/Profiles/*/storage/default/http*
|
||||
-
|
||||
category: Clear third party application data
|
||||
children:
|
||||
-
|
||||
name: Clear Adobe cache
|
||||
recommend: standard
|
||||
code: sudo rm -rfv ~/Library/Application\ Support/Adobe/Common/Media\ Cache\ Files/* &>/dev/null
|
||||
-
|
||||
name: Clear Gradle cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if [ -d "/Users/${HOST}/.gradle/caches" ]; then
|
||||
rm -rfv ~/.gradle/caches/ &> /dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear Dropbox cache
|
||||
recommend: standard
|
||||
code: |-
|
||||
if [ -d "/Users/${HOST}/Dropbox" ]; then
|
||||
sudo rm -rfv ~/Dropbox/.dropbox.cache/* &>/dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear Google Drive file stream cache
|
||||
recommend: standard
|
||||
code: |-
|
||||
killall "Google Drive File Stream"
|
||||
rm -rfv ~/Library/Application\ Support/Google/DriveFS/[0-9a-zA-Z]*/content_cache &>/dev/null
|
||||
-
|
||||
name: Clear Composer cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "composer" &> /dev/null; then
|
||||
composer clearcache &> /dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear Homebrew cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "brew" &>/dev/null; then
|
||||
brew cleanup -s &>/dev/null
|
||||
rm -rfv $(brew --cache) &>/dev/null
|
||||
brew tap --repair &>/dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear any old versions of Ruby gems
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "gem" &> /dev/null; then
|
||||
gem cleanup &>/dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear Docker
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "docker" &> /dev/null; then
|
||||
docker system prune -af
|
||||
fi
|
||||
-
|
||||
name: Clear Pyenv-VirtualEnv cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if [ "$PYENV_VIRTUALENV_CACHE_PATH" ]; then
|
||||
rm -rfv $PYENV_VIRTUALENV_CACHE_PATH &>/dev/null
|
||||
fi
|
||||
-
|
||||
name: Clear NPM cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "npm" &> /dev/null; then
|
||||
npm cache clean --force
|
||||
fi
|
||||
-
|
||||
name: Clear Yarn cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "yarn" &> /dev/null; then
|
||||
echo 'Cleanup Yarn Cache...'
|
||||
yarn cache clean --force
|
||||
fi
|
||||
-
|
||||
category: iOS Cleanup
|
||||
children:
|
||||
-
|
||||
name: Clear iOS applications
|
||||
recommend: strict
|
||||
code: rm -rfv ~/Music/iTunes/iTunes\ Media/Mobile\ Applications/* &>/dev/null
|
||||
-
|
||||
name: Clear iOS photo caches
|
||||
recommend: standard
|
||||
code: rm -rf ~/Pictures/iPhoto\ Library/iPod\ Photo\ Cache/*
|
||||
-
|
||||
name: Remove iOS Device Backups
|
||||
recommend: strict
|
||||
code: rm -rfv ~/Library/Application\ Support/MobileSync/Backup/* &>/dev/null
|
||||
-
|
||||
name: Clear iOS Simulators
|
||||
recommend: strict
|
||||
code: |-
|
||||
if type "xcrun" &>/dev/null; then
|
||||
osascript -e 'tell application "com.apple.CoreSimulator.CoreSimulatorService" to quit'
|
||||
osascript -e 'tell application "iOS Simulator" to quit'
|
||||
osascript -e 'tell application "Simulator" to quit'
|
||||
xcrun simctl shutdown all
|
||||
xcrun simctl erase all
|
||||
fi
|
||||
-
|
||||
name: Clear the list of iOS devices connected
|
||||
recommend: strict
|
||||
@@ -49,8 +291,64 @@ actions:
|
||||
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
||||
sudo rm -rfv /var/db/lockdown/*
|
||||
-
|
||||
name: Reset privacy database (remove all permissions)
|
||||
code: sudo tccutil reset All
|
||||
name: Clear XCode Derived Data and Archives
|
||||
recommend: strict
|
||||
code: |-
|
||||
rm -rfv ~/Library/Developer/Xcode/DerivedData/* &>/dev/null
|
||||
rm -rfv ~/Library/Developer/Xcode/Archives/* &>/dev/null
|
||||
rm -rfv ~/Library/Developer/Xcode/iOS Device Logs/* &>/dev/null
|
||||
-
|
||||
name: Clear DNS cache
|
||||
recommend: standard
|
||||
code: |-
|
||||
sudo dscacheutil -flushcache
|
||||
sudo killall -HUP mDNSResponder
|
||||
-
|
||||
name: Purge inactive memory
|
||||
recommend: standard
|
||||
code: sudo purge
|
||||
-
|
||||
category: Reset privacy permissions for all applications
|
||||
children:
|
||||
-
|
||||
name: Reset camera permissions
|
||||
code: tccutil reset Camera
|
||||
-
|
||||
name: Reset microphone permissions
|
||||
code: tccutil reset Microphone
|
||||
-
|
||||
name: Reset accessibility permissions
|
||||
code: tccutil reset Accessibility
|
||||
-
|
||||
name: Reset screen capture permissions
|
||||
code: tccutil reset ScreenCapture
|
||||
-
|
||||
name: Reset reminders permissions
|
||||
code: tccutil reset Reminders
|
||||
-
|
||||
name: Reset photos permissions
|
||||
code: tccutil reset Photos
|
||||
-
|
||||
name: Reset calendar permissions
|
||||
code: tccutil reset Calendar
|
||||
-
|
||||
name: Reset full disk access permissions
|
||||
code: tccutil reset SystemPolicyAllFiles
|
||||
-
|
||||
name: Reset contacts permissions
|
||||
code: tccutil reset SystemPolicyAllFiles
|
||||
-
|
||||
name: Reset desktop folder permissions
|
||||
code: tccutil reset SystemPolicyDesktopFolder
|
||||
-
|
||||
name: Reset documents folder permissions
|
||||
code: tccutil reset SystemPolicyDocumentsFolder
|
||||
-
|
||||
name: Reset downloads permissions
|
||||
code: tccutil reset SystemPolicyDownloadsFolder
|
||||
-
|
||||
name: Reset all app permissions
|
||||
code: tccutil reset All
|
||||
-
|
||||
category: Configure programs
|
||||
children:
|
||||
|
||||
@@ -476,26 +476,62 @@ actions:
|
||||
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /ENABLE
|
||||
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /ENABLE
|
||||
-
|
||||
name: Disable Webcam Telemetry (devicecensus.exe)
|
||||
recommend: standard
|
||||
docs: https://www.ghacks.net/2019/09/23/what-is-devicecensus-exe-on-windows-10-and-why-does-it-need-internet-connectivity/
|
||||
code: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /DISABLE
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /ENABLE
|
||||
category: Disable Webcam Telemetry (devicecensus.exe)
|
||||
docs:
|
||||
- https://www.ghacks.net/2019/09/23/what-is-devicecensus-exe-on-windows-10-and-why-does-it-need-internet-connectivity/
|
||||
- https://answers.microsoft.com/en-us/windows/forum/windows_10-security/devicecensusexe-and-host-process-for-windows-task/520d42a2-45c1-402a-81de-e1116ecf2538
|
||||
children:
|
||||
-
|
||||
name: Disable Application Experience (Compatibility Telemetry)
|
||||
name: Disable devicecensus.exe (telemetry) task
|
||||
recommend: standard
|
||||
code: |-
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /DISABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /DISABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /DISABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /DISABLE
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\CompatTelRunner.exe" /v "Debugger" /t REG_SZ /d "%windir%\System32\taskkill.exe" /f
|
||||
revertCode: |-
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /ENABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /ENABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /ENABLE
|
||||
schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /ENABLE
|
||||
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\CompatTelRunner.exe" /v "Debugger" /f
|
||||
code: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /disable
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Device Information\Device" /enable
|
||||
-
|
||||
name: Disable devicecensus.exe (telemetry) process
|
||||
recommend: standard
|
||||
call:
|
||||
function: KillProcessWhenItStarts
|
||||
parameters:
|
||||
processName: DeviceCensus.exe
|
||||
-
|
||||
category: Disable Compatibility Telemetry (Application Experience)
|
||||
children:
|
||||
-
|
||||
category: Disable Microsoft Compatibility Appraiser
|
||||
docs: https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||
children:
|
||||
-
|
||||
name: Disable Microsoft Compatibility Appraiser task
|
||||
recommend: standard
|
||||
code: schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /disable
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser" /enable
|
||||
-
|
||||
name: Disable CompatTelRunner.exe (Microsoft Compatibility Appraiser) process
|
||||
recommend: standard
|
||||
call:
|
||||
function: KillProcessWhenItStarts
|
||||
parameters:
|
||||
processName: CompatTelRunner.exe
|
||||
-
|
||||
name: Disable sending information to Customer Experience Improvement Program
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||
- https://answers.microsoft.com/en-us/windows/forum/windows_10-performance/permanently-disabling-windows-compatibility/6bf71583-81b0-4a74-ae2e-8fd73305aad1
|
||||
code: schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /disable
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\ProgramDataUpdater" /enable
|
||||
-
|
||||
name: Disable Application Impact Telemetry Agent task
|
||||
recommend: standard
|
||||
docs: https://www.shouldiblockit.com/aitagent.exe-6181.aspx
|
||||
code: schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /disable
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\AitAgent" /enable
|
||||
-
|
||||
name: Disable "Disable apps to improve performance" reminder
|
||||
recommend: strict
|
||||
docs: https://www.ghacks.net/2016/10/26/turn-off-the-windows-customer-experience-program/
|
||||
code: schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /disable
|
||||
revertCode: schtasks /change /TN "Microsoft\Windows\Application Experience\StartupAppTask" /enable
|
||||
-
|
||||
name: Disable telemetry in data collection policy
|
||||
recommend: standard
|
||||
@@ -1001,6 +1037,34 @@ actions:
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Deny" /t REG_SZ /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Allow" /t REG_SZ /f
|
||||
-
|
||||
category: Disable app access to voice activation
|
||||
docs: https://www.tenforums.com/tutorials/130122-allow-deny-apps-access-use-voice-activation-windows-10-a.html
|
||||
children:
|
||||
-
|
||||
name: Disable apps and Cortana to activate with voice
|
||||
recommend: standard
|
||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.AppPrivacy::LetAppsActivateWithVoice
|
||||
code: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationEnabled" /t REG_DWORD /d 0 /f
|
||||
:: Using GPO (re-activation through GUI is not possible)
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoice" /t REG_DWORD /d 2 /f
|
||||
revertCode: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationEnabled" /t REG_DWORD /d 1 /f
|
||||
:: Using GPO
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoice" /f
|
||||
-
|
||||
name: Disable apps and Cortana to activate with voice when sytem is locked
|
||||
recommend: standard
|
||||
docs: https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.AppPrivacy::LetAppsActivateWithVoiceAboveLock
|
||||
code: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationOnLockScreenEnabled" /t REG_DWORD /d 0 /f
|
||||
:: Using GPO (re-activation through GUI is not possible)
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoiceAboveLock" /t REG_DWORD /d 2 /f
|
||||
revertCode: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Settings\VoiceActivation\UserPreferenceForAllApps" /v "AgentActivationOnLockScreenEnabled" /t REG_DWORD /d 1 /f
|
||||
:: Using GPO
|
||||
reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy" /v "LetAppsActivateWithVoiceAboveLock" /f
|
||||
-
|
||||
category: Disable location access
|
||||
children:
|
||||
@@ -1081,11 +1145,61 @@ actions:
|
||||
revertCode: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
category: Disable Cortana history
|
||||
children:
|
||||
-
|
||||
name: Prevent Cortana from displaying history
|
||||
recommend: standard
|
||||
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "HistoryViewEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "HistoryViewEnabled" /f
|
||||
-
|
||||
name: Prevent Cortana from using device history
|
||||
recommend: standard
|
||||
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "DeviceHistoryEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "DeviceHistoryEnabled" /f
|
||||
-
|
||||
name: Remove the Cortana taskbar icon
|
||||
recommend: standard
|
||||
code: reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /f
|
||||
-
|
||||
name: Disable Cortana in ambient mode
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaInAmbientMode" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaInAmbientMode" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
category: Disable Cortana voice listening
|
||||
children:
|
||||
-
|
||||
name: Disable "Hey Cortana" voice activation
|
||||
recommend: standard
|
||||
code: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationOn" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationDefaultOn" /t REG_DWORD /d 0 /f
|
||||
revertCode: |-
|
||||
reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationOn" /t REG_DWORD /d 1 /f
|
||||
reg add "HKLM\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationDefaultOn" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable Cortana listening to commands on Windows key + C
|
||||
recommend: standard
|
||||
code: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "VoiceShortcut" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "VoiceShortcut" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable using Cortana even when device is locked
|
||||
recommend: standard
|
||||
code: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationEnableAboveLockscreen" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "VoiceActivationEnableAboveLockscreen" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable automatic update of Speech Data
|
||||
recommend: standard
|
||||
code: reg add "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "ModelDownloadAllowed" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKCU\Software\Microsoft\Speech_OneCore\Preferences" /v "ModelDownloadAllowed" /f
|
||||
-
|
||||
name: Disable Cortana voice support during Windows setup
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "DisableVoice" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v "DisableVoice" /f
|
||||
-
|
||||
category: Configure Windows search indexing
|
||||
children:
|
||||
@@ -1467,7 +1581,7 @@ actions:
|
||||
name: Delete NVIDIA residual telemetry files
|
||||
recommend: standard
|
||||
code: |-
|
||||
del /s %systemdrive%\System32\DriverStore\FileRepository\NvTelemetry*.dll
|
||||
del /s %SystemRoot%\System32\DriverStore\FileRepository\NvTelemetry*.dll
|
||||
rmdir /s /q "%ProgramFiles(x86)%\NVIDIA Corporation\NvTelemetry" 2>nul
|
||||
rmdir /s /q "%ProgramFiles%\NVIDIA Corporation\NvTelemetry" 2>nul
|
||||
-
|
||||
@@ -1508,45 +1622,72 @@ actions:
|
||||
name: Disable Visual Studio Code telemetry
|
||||
docs: https://code.visualstudio.com/docs/getstarted/telemetry
|
||||
recommend: standard
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'telemetry.enableTelemetry' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json; $json.PSObject.Properties.Remove('telemetry.enableTelemetry'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: telemetry.enableTelemetry
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Disable Visual Studio Code crash reporting
|
||||
docs: https://code.visualstudio.com/docs/getstarted/telemetry
|
||||
recommend: standard
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'telemetry.enableCrashReporter' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('telemetry.enableCrashReporter'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: telemetry.enableCrashReporter
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Do not run Microsoft online experiments
|
||||
docs: https://github.com/Microsoft/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173
|
||||
recommend: standard
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'workbench.enableExperiments' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('workbench.enableExperiments'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: workbench.enableExperiments
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Choose manual updates over automatic updates
|
||||
docs: https://github.com/Microsoft/vscode/blob/1aee0c194cff72d179b9f8ef324e47f34555a07d/src/vs/workbench/contrib/experiments/node/experimentService.ts#L173
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'update.mode' -Value \"manual\" -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('update.mode'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: update.mode
|
||||
powerShellValue: >-
|
||||
'manual'
|
||||
-
|
||||
name: Show Release Notes from Microsoft online service after an update
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'update.showReleaseNotes' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('update.showReleaseNotes'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: update.showReleaseNotes
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Automatically check extensions from Microsoft online service
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'extensions.autoCheckUpdates' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('extensions.autoCheckUpdates'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: extensions.autoCheckUpdates
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Fetch recommendations from a Microsoft online service
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'extensions.showRecommendationsOnlyOnDemand' -Value $true -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('extensions.showRecommendationsOnlyOnDemand'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
name: Fetch recommendations from Microsoft only on demand
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: extensions.showRecommendationsOnlyOnDemand
|
||||
powerShellValue: $true
|
||||
-
|
||||
name: Automatically fetch git commits from remote repository
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'git.autofetch' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('git.autofetch'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: git.autofetch
|
||||
powerShellValue: $false
|
||||
-
|
||||
name: Fetch package information from NPM and Bower
|
||||
code: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | Out-String | ConvertFrom-Json; $json | Add-Member -Type NoteProperty -Name 'npm.fetchOnlinePackageInfo' -Value $false -Force; $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
revertCode: Powershell -Command "$jsonfile = \"$env:APPDATA\Code\User\settings.json\"; $json = Get-Content $jsonfile | ConvertFrom-Json;$json.PSObject.Properties.Remove('npm.fetchOnlinePackageInfo'); $json | ConvertTo-Json | Set-Content $jsonfile;"
|
||||
call:
|
||||
function: SetVsCodeSetting
|
||||
parameters:
|
||||
setting: npm.fetchOnlinePackageInfo
|
||||
powerShellValue: $false
|
||||
-
|
||||
category: Disable Microsoft Office telemetry
|
||||
docs: https://docs.microsoft.com/en-us/deployoffice/compat/manage-the-privacy-of-data-monitored-by-telemetry-in-office
|
||||
@@ -2692,8 +2833,19 @@ actions:
|
||||
-
|
||||
name: Disable NetBios for all interfaces
|
||||
docs: https://10dsecurity.com/saying-goodbye-netbios/
|
||||
code: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose}"
|
||||
revertCode: Powershell -Command "$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces'; Get-ChildItem $key | foreach { Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose}"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||
Get-ChildItem $key | foreach {
|
||||
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 2 -Verbose
|
||||
}
|
||||
revertCode:
|
||||
$key = 'HKLM:SYSTEM\CurrentControlSet\services\NetBT\Parameters\Interfaces';
|
||||
Get-ChildItem $key | foreach {
|
||||
Set-ItemProperty -Path \"$key\$($_.pschildname)\" -Name NetbiosOptions -Value 0 -Verbose
|
||||
}
|
||||
-
|
||||
category: Remove bloatware
|
||||
children:
|
||||
@@ -3459,12 +3611,13 @@ actions:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.SecureAssessmentBrowser
|
||||
-
|
||||
name: Start app
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.ShellExperienceHost
|
||||
# -
|
||||
# # Not a bloatware, required for different setting windows such as WiFi and battery panes in action bar
|
||||
# name: Start app
|
||||
# call:
|
||||
# function: UninstallSystemApp
|
||||
# parameters:
|
||||
# packageName: Microsoft.Windows.ShellExperienceHost
|
||||
-
|
||||
category: Windows Feedback
|
||||
children:
|
||||
@@ -3496,12 +3649,13 @@ actions:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Windows.ContactSupport
|
||||
-
|
||||
name: Settings app
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Windows.immersivecontrolpanel
|
||||
# -
|
||||
# # Not a bloatware, required for core OS functinoality
|
||||
# name: Settings app
|
||||
# call:
|
||||
# function: UninstallSystemApp
|
||||
# parameters:
|
||||
# packageName: Windows.immersivecontrolpanel
|
||||
-
|
||||
name: Windows Print 3D app
|
||||
call:
|
||||
@@ -4138,26 +4292,36 @@ actions:
|
||||
copy "%~dpnx0" "%AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat"
|
||||
revertCode: del /f /q %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\privacy-cleanup.bat
|
||||
functions:
|
||||
-
|
||||
name: KillProcessWhenItStarts
|
||||
parameters: [ processName ]
|
||||
# https://docs.microsoft.com/en-us/previous-versions/windows/desktop/xperf/image-file-execution-options
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /t REG_SZ /d "%windir%\System32\taskkill.exe" /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\'{{ $processName }}'" /v "Debugger" /f
|
||||
-
|
||||
name: UninstallStoreApp
|
||||
parameters: [ packageName ]
|
||||
code: PowerShell -Command "Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: Get-AppxPackage '{{ $packageName }}' | Remove-AppxPackage
|
||||
revertCode:
|
||||
PowerShell -ExecutionPolicy Unrestricted -Command "
|
||||
$package = Get-AppxPackage -AllUsers '{{ $packageName }}';
|
||||
if (!$package) {
|
||||
Write-Error \"Cannot reinstall '{{ $packageName }}'\" -ErrorAction Stop
|
||||
}
|
||||
$manifest = $package.InstallLocation + '\AppxManifest.xml';
|
||||
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\" "
|
||||
Add-AppxPackage -DisableDevelopmentMode -Register \"$manifest\"
|
||||
-
|
||||
name: UninstallSystemApp
|
||||
parameters: [ packageName ]
|
||||
# It simply renames files
|
||||
# Because system apps are non removable (check: (Get-AppxPackage -AllUsers 'Windows.CBSPreview').NonRemovable)
|
||||
# Otherwise they throw 0x80070032 when trying to uninstall them
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
PowerShell -Command "
|
||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||
if (!$package) {
|
||||
Write-Host 'Not installed';
|
||||
@@ -4175,9 +4339,8 @@ functions:
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
};"
|
||||
}
|
||||
revertCode:
|
||||
PowerShell -Command "
|
||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||
if (!$package) {
|
||||
Write-Error 'App could not be found' -ErrorAction Stop;
|
||||
@@ -4193,12 +4356,17 @@ functions:
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
};"
|
||||
}
|
||||
-
|
||||
name: UninstallCapability
|
||||
parameters: [ capabilityName ]
|
||||
code: PowerShell -Command "Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online"
|
||||
revertCode: PowerShell -Command "$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'; Add-WindowsCapability -Name \"$capability.Name\" -Online"
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code: Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online
|
||||
revertCode:
|
||||
$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*';
|
||||
Add-WindowsCapability -Name \"$capability.Name\" -Online
|
||||
-
|
||||
name: RenameSystemFile
|
||||
parameters: [ filePath ]
|
||||
@@ -4220,3 +4388,31 @@ functions:
|
||||
) else (
|
||||
echo Could not find backup file "{{ $filePath }}.OLD" 1>&2
|
||||
)
|
||||
-
|
||||
name: SetVsCodeSetting
|
||||
parameters: [ setting, powerShellValue ]
|
||||
call:
|
||||
function: RunPowerShell
|
||||
parameters:
|
||||
code:
|
||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||
if (!(Test-Path $jsonfile -PathType Leaf)) {
|
||||
Write-Host \"No updates. Settings file was not at $jsonfile\";
|
||||
exit 0;
|
||||
}
|
||||
$json = Get-Content $jsonfile | Out-String | ConvertFrom-Json;
|
||||
$json | Add-Member -Type NoteProperty -Name '{{ $setting }}' -Value {{ $powerShellValue }} -Force;
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||
revertCode:
|
||||
$jsonfile = \"$env:APPDATA\Code\User\settings.json\";
|
||||
if (!(Test-Path $jsonfile -PathType Leaf)) {
|
||||
Write-Error \"Settings file could not be found at $jsonfile\" -ErrorAction Stop;
|
||||
}
|
||||
$json = Get-Content $jsonfile | ConvertFrom-Json;
|
||||
$json.PSObject.Properties.Remove('{{ $setting }}');
|
||||
$json | ConvertTo-Json | Set-Content $jsonfile;
|
||||
-
|
||||
name: RunPowerShell
|
||||
parameters: [ code, revertCode ]
|
||||
code: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $code }}"
|
||||
revertCode: PowerShell -ExecutionPolicy Unrestricted -Command "{{ $revertCode }}"
|
||||
|
||||
@@ -4,16 +4,17 @@ export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string,
|
||||
scriptName: string,
|
||||
syntax: ILanguageSyntax) {
|
||||
if (!scriptName) { throw new Error('script name is undefined'); }
|
||||
if (!syntax) { throw new Error('syntax is undefined'); }
|
||||
validateCode(scriptName, execute, syntax);
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
validateCode(execute, syntax);
|
||||
if (revert) {
|
||||
scriptName = `${scriptName} (revert)`;
|
||||
validateCode(scriptName, revert, syntax);
|
||||
try {
|
||||
validateCode(revert, syntax);
|
||||
if (execute === revert) {
|
||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||
throw new Error(`Code itself and its reverting code cannot be the same`);
|
||||
}
|
||||
} catch (err) {
|
||||
throw Error(`(revert): ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,21 +25,21 @@ export interface ILanguageSyntax {
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
|
||||
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`code of ${name} is empty or undefined`);
|
||||
throw new Error(`code is empty or undefined`);
|
||||
}
|
||||
ensureNoEmptyLines(name, code);
|
||||
ensureCodeHasUniqueLines(name, code, syntax);
|
||||
ensureNoEmptyLines(code);
|
||||
ensureCodeHasUniqueLines(code, syntax);
|
||||
}
|
||||
|
||||
function ensureNoEmptyLines(name: string, code: string): void {
|
||||
function ensureNoEmptyLines(code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`script has empty lines "${name}"`);
|
||||
throw Error(`script has empty lines`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||
const lines = code.split('\n')
|
||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||
if (lines.length === 0) {
|
||||
@@ -46,7 +47,7 @@ function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageS
|
||||
}
|
||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||
throw Error(`Duplicates detected in script :\n ${duplicateLines.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export class Clipboard {
|
||||
public static copyText(text: string): void {
|
||||
const el = document.createElement('textarea');
|
||||
|
||||
69
src/infrastructure/CodeRunner.ts
Normal file
69
src/infrastructure/CodeRunner.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import child_process from 'child_process';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export async function runCodeAsync(
|
||||
code: string, folderName: string, fileExtension: string,
|
||||
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> {
|
||||
const dir = node.path.join(node.os.tmpdir(), folderName);
|
||||
await node.fs.promises.mkdir(dir, {recursive: true});
|
||||
const filePath = node.path.join(dir, `run.${fileExtension}`);
|
||||
await node.fs.promises.writeFile(filePath, code);
|
||||
await node.fs.promises.chmod(filePath, '755');
|
||||
const command = getExecuteCommand(filePath, environment);
|
||||
node.child_process.exec(command);
|
||||
}
|
||||
|
||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||
switch (environment.os) {
|
||||
case OperatingSystem.macOS:
|
||||
return `open -a Terminal.app ${scriptPath}`;
|
||||
// Another option with graphical sudo would be
|
||||
// `osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`
|
||||
// However it runs in background
|
||||
case OperatingSystem.Windows:
|
||||
return scriptPath;
|
||||
default:
|
||||
throw Error('undefined os');
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeJs(): INodeJs {
|
||||
return { os, path, fs, child_process };
|
||||
}
|
||||
|
||||
export interface INodeJs {
|
||||
os: INodeOs;
|
||||
path: INodePath;
|
||||
fs: INodeFs;
|
||||
child_process: INodeChildProcess;
|
||||
}
|
||||
|
||||
export interface INodeOs {
|
||||
tmpdir(): string;
|
||||
}
|
||||
|
||||
export interface INodePath {
|
||||
join(...paths: string[]): string;
|
||||
}
|
||||
|
||||
export interface INodeChildProcess {
|
||||
exec(command: string): void;
|
||||
}
|
||||
|
||||
export interface INodeFs {
|
||||
readonly promises: INodeFsPromises;
|
||||
}
|
||||
|
||||
interface INodeFsPromisesMakeDirectoryOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
interface INodeFsPromises { // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v13/fs.d.ts
|
||||
chmod(path: string, mode: string | number): Promise<void>;
|
||||
mkdir(path: string, options: INodeFsPromisesMakeDirectoryOptions): Promise<string>;
|
||||
writeFile(path: string, data: string): Promise<void>;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EventHandler, ISignal } from './ISignal';
|
||||
import { IEventSubscription } from './ISubscription';
|
||||
import { EventHandler, IEventSource, IEventSubscription } from './IEventSource';
|
||||
|
||||
export class Signal<T> implements ISignal<T> {
|
||||
export class EventSource<T> implements IEventSource<T> {
|
||||
private handlers = new Map<number, EventHandler<T>>();
|
||||
|
||||
public on(handler: EventHandler<T>): IEventSubscription {
|
||||
12
src/infrastructure/Events/EventSubscriptionCollection.ts
Normal file
12
src/infrastructure/Events/EventSubscriptionCollection.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IEventSubscription } from './IEventSource';
|
||||
|
||||
export class EventSubscriptionCollection {
|
||||
private readonly subscriptions = new Array<IEventSubscription>();
|
||||
public register(...subscriptions: IEventSubscription[]) {
|
||||
this.subscriptions.push(...subscriptions);
|
||||
}
|
||||
public unsubscribeAll() {
|
||||
this.subscriptions.forEach((listener) => listener.unsubscribe());
|
||||
this.subscriptions.splice(0, this.subscriptions.length);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IEventSubscription } from './ISubscription';
|
||||
export interface ISignal<T> {
|
||||
export interface IEventSource<T> {
|
||||
on(handler: EventHandler<T>): IEventSubscription;
|
||||
}
|
||||
|
||||
export interface IEventSubscription {
|
||||
unsubscribe(): void;
|
||||
}
|
||||
|
||||
export type EventHandler<T> = (data: T) => void;
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface IEventSubscription {
|
||||
unsubscribe(): void;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Signal } from '../Events/Signal';
|
||||
import { EventSource } from '../Events/EventSource';
|
||||
|
||||
export class AsyncLazy<T> {
|
||||
private valueCreated = new Signal();
|
||||
private valueCreated = new EventSource();
|
||||
private isValueCreated = false;
|
||||
private isCreatingValue = false;
|
||||
private value: T | undefined;
|
||||
@@ -15,7 +15,7 @@ export class AsyncLazy<T> {
|
||||
public async getValueAsync(): Promise<T> {
|
||||
// If value is already created, return the value directly
|
||||
if (this.isValueCreated) {
|
||||
return Promise.resolve(this.value as T);
|
||||
return Promise.resolve(this.value);
|
||||
}
|
||||
// If value is being created, wait until the value is created and then return it.
|
||||
if (this.isCreatingValue) {
|
||||
|
||||
@@ -7,8 +7,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf, faPlay, faArrowsAltH } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export class IconBootstrapper implements IVueBootstrapper {
|
||||
public bootstrap(vue: VueConstructor): void {
|
||||
@@ -24,9 +23,12 @@ export class IconBootstrapper implements IVueBootstrapper {
|
||||
faTimes,
|
||||
faFileDownload, faSave,
|
||||
faCopy,
|
||||
faPlay,
|
||||
faSearch,
|
||||
faBatteryFull, faBatteryHalf,
|
||||
faInfoCircle);
|
||||
faInfoCircle,
|
||||
faArrowsAltH,
|
||||
);
|
||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="instructions">
|
||||
<!-- <p>
|
||||
<p>
|
||||
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||
</p> -->
|
||||
</p>
|
||||
<p>
|
||||
<ol>
|
||||
<li>
|
||||
@@ -73,36 +73,33 @@
|
||||
</li>
|
||||
</ol>
|
||||
</p>
|
||||
<!-- <p>
|
||||
<p>
|
||||
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||
</p> -->
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import Code from './Code.vue';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Code,
|
||||
},
|
||||
})
|
||||
export default class MacOsInstructions extends StatefulVue {
|
||||
export default class MacOsInstructions extends Vue {
|
||||
@Prop() public fileName: string;
|
||||
public appName = '';
|
||||
public macOsDownloadUrl = '';
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.appName = app.info.name;
|
||||
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||
}
|
||||
protected handleCollectionState(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
<IconButton
|
||||
v-if="this.canRun"
|
||||
text="Run"
|
||||
v-on:click="executeCodeAsync"
|
||||
icon-prefix="fas" icon-name="play">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||
v-on:click="saveCodeAsync"
|
||||
@@ -35,11 +41,11 @@ import MacOsInstructions from './MacOsInstructions.vue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { runCodeAsync } from '@/infrastructure/CodeRunner';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -50,13 +56,12 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public readonly macOsModalName = 'macos-instructions';
|
||||
|
||||
public readonly isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
public canRun = false;
|
||||
public hasCode = false;
|
||||
public isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
public isMacOsCollection = false;
|
||||
public fileName = '';
|
||||
|
||||
private codeListener: IEventSubscription;
|
||||
|
||||
public async copyCodeAsync() {
|
||||
const code = await this.getCurrentCodeAsync();
|
||||
Clipboard.copyText(code.current);
|
||||
@@ -68,16 +73,13 @@ export default class TheCodeButtons extends StatefulVue {
|
||||
this.$modal.show(this.macOsModalName);
|
||||
}
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
public async executeCodeAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
await executeCodeAsync(context);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.canRun = this.isDesktopVersion && newState.collection.os === Environment.CurrentEnvironment.os;
|
||||
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||
this.fileName = buildFileName(newState.collection.scripting);
|
||||
this.react(newState.code);
|
||||
@@ -90,12 +92,10 @@ export default class TheCodeButtons extends StatefulVue {
|
||||
}
|
||||
private async react(code: IApplicationCode) {
|
||||
this.hasCode = code.current && code.current.length > 0;
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
this.codeListener = code.changed.on((newCode) => {
|
||||
this.events.unsubscribeAll();
|
||||
this.events.register(code.changed.on((newCode) => {
|
||||
this.hasCode = newCode && newCode.code.length > 0;
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,15 @@ function buildFileName(scripting: IScriptingDefinition) {
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
async function executeCodeAsync(context: IApplicationContext) {
|
||||
await runCodeAsync(
|
||||
/*code*/ context.state.code.current,
|
||||
/*appName*/ context.app.info.name,
|
||||
/*fileExtension*/ context.state.collection.scripting.fileExtension,
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1,56 +1,55 @@
|
||||
<template>
|
||||
<div :id="editorId" class="code-area" ></div>
|
||||
<Responsive v-on:sizeChanged="sizeChanged()">
|
||||
<div
|
||||
:id="editorId"
|
||||
class="code-area"
|
||||
></div>
|
||||
</Responsive>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import ace from 'ace-builds';
|
||||
import 'ace-builds/webpack-resolver';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||
import Responsive from '@/presentation/Responsive.vue';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
components: {
|
||||
Responsive,
|
||||
},
|
||||
})
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
private currentMarkerId?: number;
|
||||
private codeListener: IEventSubscription;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public destroyed() {
|
||||
this.unsubscribeCodeListening();
|
||||
this.destroyEditor();
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
public sizeChanged() {
|
||||
if (this.editor) {
|
||||
this.editor.resize();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.destroyEditor();
|
||||
this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language);
|
||||
const appCode = newState.code;
|
||||
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
|
||||
this.unsubscribeCodeListening();
|
||||
this.subscribe(appCode);
|
||||
this.events.unsubscribeAll();
|
||||
this.events.register(appCode.changed.on((code) => this.updateCodeAsync(code)));
|
||||
}
|
||||
|
||||
private subscribe(appCode: IApplicationCode) {
|
||||
this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code));
|
||||
}
|
||||
private unsubscribeCodeListening() {
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
private async updateCodeAsync(event: ICodeChangedEvent) {
|
||||
this.removeCurrentHighlighting();
|
||||
if (event.isEmpty()) {
|
||||
@@ -60,7 +59,6 @@ export default class TheCodeArea extends StatefulVue {
|
||||
return;
|
||||
}
|
||||
this.editor.setValue(event.code, 1);
|
||||
|
||||
if (event.addedScripts && event.addedScripts.length) {
|
||||
this.reactToChanges(event, event.addedScripts);
|
||||
} else if (event.changedScripts && event.changedScripts.length) {
|
||||
@@ -99,6 +97,7 @@ export default class TheCodeArea extends StatefulVue {
|
||||
private destroyEditor() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
this.editor = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +110,7 @@ function initializeEditor(theme: string, editorId: string, language: ScriptingLa
|
||||
editor.setTheme(`ace/theme/${theme}`);
|
||||
editor.setReadOnly(true);
|
||||
editor.setAutoScrollEditorIntoView(true);
|
||||
editor.setShowPrintMargin(false); // hides vertical line
|
||||
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
||||
return editor;
|
||||
}
|
||||
@@ -145,17 +145,17 @@ function getDefaultCode(language: ScriptingLanguage): string {
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
.code-area {
|
||||
width: 100%;
|
||||
max-height: 1000px;
|
||||
::v-deep .code-area {
|
||||
min-height: 200px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
&__highlight {
|
||||
background-color:$accent;
|
||||
background-color: $accent;
|
||||
opacity: 0.2; // having procent fails in production (minified) build
|
||||
position:absolute;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
src/presentation/Responsive.vue
Normal file
75
src/presentation/Responsive.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div ref="containerElement" class="container">
|
||||
<slot ref="containerElement"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Emit } from 'vue-property-decorator';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { throttle } from './Throttle';
|
||||
|
||||
@Component
|
||||
export default class Responsive extends Vue {
|
||||
private width: number;
|
||||
private height: number;
|
||||
private observer: ResizeObserver;
|
||||
private get container(): HTMLElement { return this.$refs.containerElement as HTMLElement; }
|
||||
|
||||
public mounted() {
|
||||
this.width = this.container.offsetWidth;
|
||||
this.height = this.container.offsetHeight;
|
||||
const resizeCallback = throttle(() => this.updateSize(), 200);
|
||||
this.observer = new ResizeObserver(resizeCallback);
|
||||
this.observer.observe(this.container);
|
||||
this.fireChangeEvents();
|
||||
}
|
||||
public updateSize() {
|
||||
let sizeChanged = false;
|
||||
if (this.isWidthChanged()) {
|
||||
this.updateWidth(this.container.offsetWidth);
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (this.isHeightChanged()) {
|
||||
this.updateHeight(this.container.offsetHeight);
|
||||
sizeChanged = true;
|
||||
}
|
||||
if (sizeChanged) {
|
||||
this.$emit('sizeChanged');
|
||||
}
|
||||
}
|
||||
@Emit('widthChanged') public updateWidth(width: number) {
|
||||
this.width = width;
|
||||
}
|
||||
@Emit('heightChanged') public updateHeight(height: number) {
|
||||
this.height = height;
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private fireChangeEvents() {
|
||||
this.updateWidth(this.container.offsetWidth);
|
||||
this.updateHeight(this.container.offsetHeight);
|
||||
this.$emit('sizeChanged');
|
||||
}
|
||||
private isWidthChanged(): boolean {
|
||||
return this.width !== this.container.offsetWidth;
|
||||
}
|
||||
private isHeightChanged(): boolean {
|
||||
return this.height !== this.container.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block; // if inline then it has no height or weight
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<Responsive v-on:widthChanged="width = $event">
|
||||
<!-- <div id="responsivity-debug">
|
||||
Width: {{ width || 'undefined' }}
|
||||
Size: <span v-if="width <= 500">small</span><span v-if="width > 500 && width < 750">medium</span><span v-if="width >= 750">big</span>
|
||||
</div> -->
|
||||
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
|
||||
<CardListItem
|
||||
class="card"
|
||||
v-bind:class="{
|
||||
'small-screen': width <= 500,
|
||||
'medium-screen': width > 500 && width < 750,
|
||||
'big-screen': width >= 750
|
||||
}"
|
||||
v-for="categoryId of categoryIds"
|
||||
:data-category="categoryId"
|
||||
v-bind:key="categoryId"
|
||||
@@ -12,24 +21,26 @@
|
||||
</CardListItem>
|
||||
</div>
|
||||
<div v-else class="error">Something went bad 😢</div>
|
||||
</div>
|
||||
</Responsive>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import CardListItem from './CardListItem.vue';
|
||||
import Responsive from '@/presentation/Responsive.vue';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { hasDirective } from './NonCollapsingDirective';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
CardListItem,
|
||||
Responsive,
|
||||
},
|
||||
})
|
||||
export default class CardList extends StatefulVue {
|
||||
public width: number = 0;
|
||||
public categoryIds: number[] = [];
|
||||
public activeCategoryId?: number = null;
|
||||
|
||||
@@ -45,9 +56,6 @@ export default class CardList extends StatefulVue {
|
||||
this.activeCategoryId = isExpanded ? categoryId : undefined;
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.setCategories(newState.collection.actions);
|
||||
this.activeCategoryId = undefined;
|
||||
@@ -79,6 +87,7 @@ export default class CardList extends StatefulVue {
|
||||
flex-flow: row wrap;
|
||||
font-family: $main-font;
|
||||
}
|
||||
|
||||
.error {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -50,17 +49,11 @@ export default class CardListItem extends StatefulVue {
|
||||
public isAnyChildSelected = false;
|
||||
public areAllChildrenSelected = false;
|
||||
|
||||
private selectionChangedListener: IEventSubscription;
|
||||
|
||||
public async mounted() {
|
||||
this.updateStateAsync(this.categoryId);
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.selectionChangedListener = context.state.selection.changed.on(() => this.updateStateAsync(this.categoryId));
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.selectionChangedListener) {
|
||||
this.selectionChangedListener.unsubscribe();
|
||||
}
|
||||
this.events.register(context.state.selection.changed.on(
|
||||
() => this.updateSelectionIndicatorsAsync(this.categoryId)));
|
||||
await this.updateStateAsync(this.categoryId);
|
||||
}
|
||||
@Emit('selected')
|
||||
public onSelected(isExpanded: boolean) {
|
||||
@@ -81,19 +74,22 @@ export default class CardListItem extends StatefulVue {
|
||||
@Watch('categoryId')
|
||||
public async updateStateAsync(value: |number) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const category = !value ? undefined : context.state.collection.findCategory(this.categoryId);
|
||||
const category = !value ? undefined : context.state.collection.findCategory(value);
|
||||
this.cardTitle = category ? category.name : undefined;
|
||||
const currentSelection = context.state.selection;
|
||||
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
|
||||
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false;
|
||||
}
|
||||
protected initialize(): void {
|
||||
return;
|
||||
await this.updateSelectionIndicatorsAsync(value);
|
||||
}
|
||||
|
||||
protected handleCollectionState(): void {
|
||||
// No need, as categoryId will be updated instead
|
||||
return;
|
||||
}
|
||||
|
||||
private async updateSelectionIndicatorsAsync(categoryId: number) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const selection = context.state.selection;
|
||||
const category = context.state.collection.findCategory(categoryId);
|
||||
this.isAnyChildSelected = category ? selection.isAnySelected(category) : false;
|
||||
this.areAllChildrenSelected = category ? selection.areAllSelected(category) : false;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -110,12 +106,7 @@ $expanded-margin-top: 30px;
|
||||
|
||||
.card {
|
||||
margin: 15px;
|
||||
width: calc((100% / 3) - #{$card-line-break-width});
|
||||
transition: all 0.2s ease-in-out;
|
||||
// Media queries for stacking cards
|
||||
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
|
||||
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
||||
|
||||
&__inner {
|
||||
padding: $card-padding $card-padding 0 $card-padding;
|
||||
@@ -245,31 +236,32 @@ $expanded-margin-top: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
||||
.card:nth-of-type(3n+2) .card__expander {
|
||||
margin-left: calc(-100% - #{$card-line-break-width});
|
||||
@mixin adaptive-card($cards-in-row) {
|
||||
&.card {
|
||||
width: calc((100% / #{$cards-in-row}) - #{$card-line-break-width});
|
||||
@for $nth-card from 2 through $cards-in-row {
|
||||
&:nth-of-type(#{$cards-in-row}n+#{$nth-card}) {
|
||||
.card__expander {
|
||||
$card-left: -100% * ($nth-card - 1);
|
||||
$additional-space: $card-line-break-width * ($nth-card - 1);
|
||||
margin-left: calc(#{$card-left} - #{$additional-space});
|
||||
}
|
||||
.card:nth-of-type(3n+3) .card__expander {
|
||||
margin-left: calc(-200% - (#{$card-line-break-width} * 2));
|
||||
}
|
||||
.card:nth-of-type(3n+4) {
|
||||
}
|
||||
// Ensure new line after last row
|
||||
$card-after-last: $cards-in-row + 1;
|
||||
&:nth-of-type(#{$cards-in-row}n+#{$card-after-last}) {
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(300% + (#{$card-line-break-width} * 2));
|
||||
$all-cards-width: 100% * $cards-in-row;
|
||||
$card-padding: $card-line-break-width * ($cards-in-row - 1);
|
||||
width: calc(#{$all-cards-width} + #{$card-padding});
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
||||
.card:nth-of-type(2n+2) .card__expander {
|
||||
margin-left: calc(-100% - #{$card-line-break-width});
|
||||
}
|
||||
.card:nth-of-type(2n+3) {
|
||||
clear: left;
|
||||
}
|
||||
.card__expander {
|
||||
width: calc(200% + #{$card-line-break-width});
|
||||
}
|
||||
}
|
||||
.big-screen { @include adaptive-card(3); }
|
||||
.medium-screen { @include adaptive-card(2); }
|
||||
.small-screen { @include adaptive-card(1); }
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DirectiveOptions } from 'vue';
|
||||
|
||||
const attributeName = 'data-interactionDoesNotCollapse';
|
||||
const attributeName = 'data-interaction-does-not-collapse';
|
||||
|
||||
export function hasDirective(el: Element): boolean {
|
||||
if (el.hasAttribute(attributeName)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="part select">Select:</div>
|
||||
<div class="part">Select:</div>
|
||||
<div class="part">
|
||||
<div class="part">
|
||||
<SelectableOption
|
||||
@@ -56,7 +56,6 @@ import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
enum SelectionState {
|
||||
Standard,
|
||||
@@ -82,9 +81,6 @@ export default class TheSelector extends StatefulVue {
|
||||
selectType(context.state, type);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.updateSelections(newState);
|
||||
newState.selection.changed.on(() => this.updateSelections(newState));
|
||||
@@ -177,5 +173,4 @@ function areAllSelected(
|
||||
}
|
||||
font-family: $normal-font;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- <div>OS:</div> -->
|
||||
<div class="os-list">
|
||||
<div v-for="os in this.allOses" :key="os.name">
|
||||
<span
|
||||
class="name"
|
||||
class="os-name"
|
||||
v-bind:class="{ 'current': currentOs === os.os }"
|
||||
v-on:click="changeOsAsync(os.os)">
|
||||
{{ os.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -16,23 +19,24 @@ import { Component } from 'vue-property-decorator';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
|
||||
@Component
|
||||
export default class TheOsChanger extends StatefulVue {
|
||||
public allOses: Array<{ name: string, os: OperatingSystem }> = [];
|
||||
public currentOs: OperatingSystem = undefined;
|
||||
public currentOs: OperatingSystem = OperatingSystem.Unknown;
|
||||
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.allOses = app.getSupportedOsList()
|
||||
.map((os) => ({ os, name: renderOsName(os) }));
|
||||
}
|
||||
public async changeOsAsync(newOs: OperatingSystem) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
context.changeContext(newOs);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
this.allOses = app.getSupportedOsList()
|
||||
.map((os) => ({ os, name: renderOsName(os) }));
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.currentOs = newState.os;
|
||||
this.$forceUpdate(); // v-bind:class is not updated otherwise
|
||||
}
|
||||
@@ -41,7 +45,7 @@ export default class TheOsChanger extends StatefulVue {
|
||||
function renderOsName(os: OperatingSystem): string {
|
||||
switch (os) {
|
||||
case OperatingSystem.Windows: return 'Windows';
|
||||
case OperatingSystem.macOS: return 'macOS (preview)';
|
||||
case OperatingSystem.macOS: return 'macOS';
|
||||
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||
}
|
||||
}
|
||||
@@ -54,11 +58,14 @@ function renderOsName(os: OperatingSystem): string {
|
||||
font-family: $normal-font;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.os-list {
|
||||
display: flex;
|
||||
margin-left: 0.25rem;
|
||||
div + div::before {
|
||||
content: "|";
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.name {
|
||||
.os-name {
|
||||
&:not(.current) {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
@@ -70,5 +77,6 @@ function renderOsName(os: OperatingSystem): string {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
77
src/presentation/Scripts/Menu/TheScriptsMenu.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<TheSelector class="item" />
|
||||
<TheOsChanger class="item" />
|
||||
<TheGrouper
|
||||
class="item"
|
||||
v-on:groupingChanged="$emit('groupingChanged', $event)"
|
||||
v-if="!this.isSearching" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import TheOsChanger from './TheOsChanger.vue';
|
||||
import TheSelector from './Selector/TheSelector.vue';
|
||||
import TheGrouper from './Grouping/TheGrouper.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
TheSelector,
|
||||
TheOsChanger,
|
||||
TheGrouper,
|
||||
},
|
||||
})
|
||||
export default class TheScriptsMenu extends StatefulVue {
|
||||
public isSearching = false;
|
||||
|
||||
private listeners = new Array<IEventSubscription>();
|
||||
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
protected initialize(): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.subscribe(newState);
|
||||
}
|
||||
|
||||
private subscribe(state: ICategoryCollectionState) {
|
||||
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
}));
|
||||
state.filter.filtered.on(() => {
|
||||
this.isSearching = true;
|
||||
});
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.item {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 5px 0 5px;
|
||||
&:first-child {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,8 +27,6 @@ import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -43,7 +41,6 @@ export default class ScriptsTree extends StatefulVue {
|
||||
public filterText?: string = null;
|
||||
|
||||
private filtered?: IFilterResult;
|
||||
private listeners = new Array<IEventSubscription>();
|
||||
|
||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
@@ -75,31 +72,24 @@ export default class ScriptsTree extends StatefulVue {
|
||||
|| this.filtered.categoryMatches.some(
|
||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||
}
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected async handleCollectionState(newState: ICategoryCollectionState) {
|
||||
this.setCurrentFilter(newState.filter.currentFilter);
|
||||
if (!this.categoryId) {
|
||||
this.nodes = parseAllCategories(newState.collection);
|
||||
}
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState);
|
||||
this.events.unsubscribeAll();
|
||||
this.subscribeState(newState);
|
||||
}
|
||||
|
||||
private subscribe(state: ICategoryCollectionState) {
|
||||
this.listeners.push(state.selection.changed.on(this.handleSelectionChanged));
|
||||
this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved));
|
||||
this.listeners.push(state.filter.filtered.on(this.handleFiltered));
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
private subscribeState(state: ICategoryCollectionState) {
|
||||
this.events.register(
|
||||
state.selection.changed.on(this.handleSelectionChanged),
|
||||
state.filter.filterRemoved.on(this.handleFilterRemoved),
|
||||
state.filter.filtered.on(this.handleFiltered),
|
||||
);
|
||||
}
|
||||
|
||||
private setCurrentFilter(currentFilter: IFilterResult | undefined) {
|
||||
if (!currentFilter) {
|
||||
this.handleFilterRemoved();
|
||||
|
||||
@@ -13,23 +13,20 @@
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { IReverter } from './Reverter/IReverter';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { INode } from './INode';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { getReverter } from './Reverter/ReverterFactory';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { IReverter } from './Reverter/IReverter';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { INode } from './INode';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { getReverter } from './Reverter/ReverterFactory';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
|
||||
@Component
|
||||
export default class RevertToggle extends StatefulVue {
|
||||
@Component
|
||||
export default class RevertToggle extends StatefulVue {
|
||||
@Prop() public node: INode;
|
||||
public isReverted = false;
|
||||
|
||||
private handler: IReverter;
|
||||
private selectionChangeListener: IEventSubscription;
|
||||
|
||||
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
@@ -40,22 +37,16 @@
|
||||
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.updateStatus(newState.selection.selectedScripts);
|
||||
if (this.selectionChangeListener) {
|
||||
this.selectionChangeListener.unsubscribe();
|
||||
}
|
||||
this.selectionChangeListener = newState.selection.changed.on(
|
||||
(scripts) => this.updateStatus(scripts));
|
||||
this.events.unsubscribeAll();
|
||||
this.events.register(newState.selection.changed.on((scripts) => this.updateStatus(scripts)));
|
||||
}
|
||||
|
||||
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
|
||||
this.isReverted = this.handler.getState(scripts);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
71
src/presentation/Scripts/Slider/Handle.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
class="handle"
|
||||
:style="{ cursor: cursorCssValue }"
|
||||
@mousedown="startResize">
|
||||
<div class="line"></div>
|
||||
<font-awesome-icon
|
||||
class="image"
|
||||
:icon="['fas', 'arrows-alt-h']"
|
||||
/> <!-- exchange-alt arrows-alt-h-->
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class Handle extends Vue {
|
||||
public readonly cursorCssValue = 'ew-resize';
|
||||
private initialX: number = undefined;
|
||||
|
||||
public startResize(event: MouseEvent): void {
|
||||
this.initialX = event.clientX;
|
||||
document.body.style.setProperty('cursor', this.cursorCssValue);
|
||||
document.addEventListener('mousemove', this.resize);
|
||||
window.addEventListener('mouseup', this.stopResize);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
public resize(event: MouseEvent): void {
|
||||
const displacementX = event.clientX - this.initialX;
|
||||
this.$emit('resized', displacementX);
|
||||
this.initialX = event.clientX;
|
||||
}
|
||||
public stopResize(): void {
|
||||
document.body.style.removeProperty('cursor');
|
||||
document.removeEventListener('mousemove', this.resize);
|
||||
window.removeEventListener('mouseup', this.stopResize);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.handle {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
.line {
|
||||
background: $gray;
|
||||
}
|
||||
.image {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
background: $dark-gray;
|
||||
width: 3px;
|
||||
}
|
||||
.image {
|
||||
color: $dark-gray;
|
||||
}
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
53
src/presentation/Scripts/Slider/HorizontalResizeSlider.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="slider">
|
||||
<div class="left" ref="leftElement">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<Handle class="handle" @resized="onResize($event)" />
|
||||
<div class="right">
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import Handle from './Handle.vue';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Handle,
|
||||
},
|
||||
})
|
||||
export default class HorizontalResizeSlider extends Vue {
|
||||
private get left(): HTMLElement { return this.$refs.leftElement as HTMLElement; }
|
||||
|
||||
public onResize(displacementX: number): void {
|
||||
const leftWidth = this.left.offsetWidth + displacementX;
|
||||
this.left.style.width = `${leftWidth}px`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/presentation/styles/media.scss";
|
||||
|
||||
.slider {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.right {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $vertical-view-breakpoint) {
|
||||
.slider {
|
||||
flex-direction: column;
|
||||
.left {
|
||||
width: auto !important;
|
||||
}
|
||||
.handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
49
src/presentation/Scripts/TheScriptArea.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="scripts">
|
||||
<TheScriptsMenu v-on:groupingChanged="grouping = $event" />
|
||||
<HorizontalResizeSlider class="row">
|
||||
<template v-slot:left>
|
||||
<TheScriptsList :grouping="grouping" />
|
||||
</template>
|
||||
<template v-slot:right>
|
||||
<TheCodeArea theme="xcode" />
|
||||
</template>
|
||||
</HorizontalResizeSlider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import TheCodeArea from '@/presentation/Code/TheCodeArea.vue';
|
||||
import TheScriptsList from '@/presentation/Scripts/TheScriptsList.vue';
|
||||
import TheScriptsMenu from '@/presentation/Scripts/Menu/TheScriptsMenu.vue';
|
||||
import HorizontalResizeSlider from '@/presentation/Scripts/Slider/HorizontalResizeSlider.vue';
|
||||
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
TheCodeArea,
|
||||
TheScriptsList,
|
||||
TheScriptsMenu,
|
||||
HorizontalResizeSlider,
|
||||
},
|
||||
})
|
||||
export default class TheScriptArea extends Vue {
|
||||
public grouping = Grouping.Cards;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scripts {
|
||||
> * + * {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
::v-deep .left {
|
||||
width: 55%; // initial width
|
||||
min-width: 20%;
|
||||
}
|
||||
::v-deep .right {
|
||||
min-width: 20%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,195 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="heading">
|
||||
<TheSelector class="item"/>
|
||||
<TheOsChanger class="item"/>
|
||||
<TheGrouper
|
||||
class="item"
|
||||
v-on:groupingChanged="onGroupingChanged($event)"
|
||||
v-if="!this.isSearching" />
|
||||
</div>
|
||||
<div class="scripts">
|
||||
<div v-if="!isSearching">
|
||||
<CardList v-if="currentGrouping === Grouping.Cards"/>
|
||||
<div class="tree" v-if="currentGrouping === Grouping.None">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else> <!-- Searching -->
|
||||
<div class="search">
|
||||
<div class="search__query">
|
||||
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
||||
<div class="search__query__close-button">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
v-on:click="clearSearchQueryAsync()"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
|
||||
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> ✨</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="searchHasMatches" class="tree tree--searching">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
||||
import TheOsChanger from '@/presentation/Scripts/TheOsChanger.vue';
|
||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Grouping } from './Grouping/Grouping';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
/** Shows content of single category or many categories */
|
||||
@Component({
|
||||
components: {
|
||||
TheGrouper,
|
||||
TheSelector,
|
||||
ScriptsTree,
|
||||
CardList,
|
||||
TheOsChanger,
|
||||
},
|
||||
filters: {
|
||||
threeDotsTrim(query: string) {
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
}
|
||||
return `${query.substr(0, threshold)}...`;
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class TheScripts extends StatefulVue {
|
||||
public repositoryUrl = '';
|
||||
public Grouping = Grouping; // Make it accessible from view
|
||||
public currentGrouping = Grouping.Cards;
|
||||
public searchQuery = '';
|
||||
public isSearching = false;
|
||||
public searchHasMatches = false;
|
||||
|
||||
private listeners = new Array<IEventSubscription>();
|
||||
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
public async clearSearchQueryAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const filter = context.state.filter;
|
||||
filter.removeFilter();
|
||||
}
|
||||
public onGroupingChanged(group: Grouping) {
|
||||
this.currentGrouping = group;
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
this.repositoryUrl = app.info.repositoryWebUrl;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState);
|
||||
}
|
||||
|
||||
private subscribe(state: ICategoryCollectionState) {
|
||||
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
}));
|
||||
state.filter.filtered.on((result: IFilterResult) => {
|
||||
this.searchQuery = result.query;
|
||||
this.isSearching = true;
|
||||
this.searchHasMatches = result.hasAnyMatches();
|
||||
});
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
$inner-margin: 4px;
|
||||
|
||||
.scripts {
|
||||
margin-top: $inner-margin;
|
||||
.tree {
|
||||
padding-left: 3%;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
&--searching {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $slate;
|
||||
&__query {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
color: $gray;
|
||||
&__close-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
margin-left: 0.25rem;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-no-matches {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
word-break:break-word;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
padding:10px;
|
||||
text-align:center;
|
||||
> div {
|
||||
padding-bottom:13px;
|
||||
}
|
||||
a {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-top: $inner-margin;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.item {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 5px 0 5px;
|
||||
&:first-child {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
159
src/presentation/Scripts/TheScriptsList.vue
Normal file
159
src/presentation/Scripts/TheScriptsList.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="scripts">
|
||||
<div v-if="!isSearching">
|
||||
<CardList v-if="grouping === Grouping.Cards"/>
|
||||
<div class="tree" v-if="grouping === Grouping.None">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else> <!-- Searching -->
|
||||
<div class="search">
|
||||
<div class="search__query">
|
||||
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
|
||||
<div class="search__query__close-button">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
v-on:click="clearSearchQueryAsync()"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!searchHasMatches" class="search-no-matches">
|
||||
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
|
||||
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> ✨</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="searchHasMatches" class="tree tree--searching">
|
||||
<ScriptsTree />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import TheGrouper from '@/presentation/Scripts/Menu/Grouping/TheGrouper.vue';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Grouping } from '@/presentation/Scripts/Menu/Grouping/Grouping';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
|
||||
/** Shows content of single category or many categories */
|
||||
@Component({
|
||||
components: {
|
||||
TheGrouper,
|
||||
ScriptsTree,
|
||||
CardList,
|
||||
},
|
||||
filters: {
|
||||
threeDotsTrim(query: string) {
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
}
|
||||
return `${query.substr(0, threshold)}...`;
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class TheScriptsList extends StatefulVue {
|
||||
@Prop() public grouping: Grouping;
|
||||
|
||||
public repositoryUrl = '';
|
||||
public Grouping = Grouping; // Make it accessible from the view
|
||||
public searchQuery = '';
|
||||
public isSearching = false;
|
||||
public searchHasMatches = false;
|
||||
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.repositoryUrl = app.info.repositoryWebUrl;
|
||||
}
|
||||
public async clearSearchQueryAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const filter = context.state.filter;
|
||||
filter.removeFilter();
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.events.unsubscribeAll();
|
||||
this.subscribeState(newState);
|
||||
}
|
||||
|
||||
private subscribeState(state: ICategoryCollectionState) {
|
||||
this.events.register(
|
||||
state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
}),
|
||||
state.filter.filtered.on((result: IFilterResult) => {
|
||||
this.searchQuery = result.query;
|
||||
this.isSearching = true;
|
||||
this.searchHasMatches = result.hasAnyMatches();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
@import "@/presentation/styles/media.scss";
|
||||
|
||||
$inner-margin: 4px;
|
||||
|
||||
.scripts {
|
||||
margin-top: $inner-margin;
|
||||
@media screen and (min-width: $vertical-view-breakpoint) { // so the current code is always visible
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
.tree {
|
||||
padding-left: 3%;
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
&--searching {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $slate;
|
||||
&__query {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 1em;
|
||||
color: $gray;
|
||||
&__close-button {
|
||||
cursor: pointer;
|
||||
font-size: 1.25em;
|
||||
margin-left: 0.25rem;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-no-matches {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
word-break:break-word;
|
||||
text-transform: uppercase;
|
||||
color: $light-gray;
|
||||
font-size: 1.5em;
|
||||
padding:10px;
|
||||
text-align:center;
|
||||
> div {
|
||||
padding-bottom:13px;
|
||||
}
|
||||
a {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,33 +1,30 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { buildContext } from '@/application/Context/ApplicationContextProvider';
|
||||
import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '../infrastructure/Events/ISubscription';
|
||||
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
|
||||
import { IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { EventSubscriptionCollection } from '../infrastructure/Events/EventSubscriptionCollection';
|
||||
|
||||
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
|
||||
@Component
|
||||
export abstract class StatefulVue extends Vue {
|
||||
public static instance = new AsyncLazy<IApplicationContext>(
|
||||
() => Promise.resolve(buildContext()));
|
||||
private static readonly instance = new AsyncLazy<IApplicationContext>(() => buildContextAsync());
|
||||
|
||||
private listener: IEventSubscription;
|
||||
protected readonly events = new EventSubscriptionCollection();
|
||||
|
||||
private readonly ownEvents = new EventSubscriptionCollection();
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event));
|
||||
this.initialize(context.app);
|
||||
this.ownEvents.register(context.contextChanged.on((event) => this.handleStateChangedEvent(event)));
|
||||
this.handleCollectionState(context.state, undefined);
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.listener) {
|
||||
this.listener.unsubscribe();
|
||||
}
|
||||
this.ownEvents.unsubscribeAll();
|
||||
this.events.unsubscribeAll();
|
||||
}
|
||||
|
||||
protected abstract initialize(app: IApplication): void;
|
||||
protected abstract handleCollectionState(
|
||||
newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
|
||||
protected getCurrentContextAsync(): Promise<IApplicationContext> {
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
|
||||
@Component
|
||||
export default class DownloadUrlListItem extends StatefulVue {
|
||||
export default class DownloadUrlListItem extends Vue {
|
||||
@Prop() public operatingSystem!: OperatingSystem;
|
||||
|
||||
public downloadUrl: string = '';
|
||||
@@ -38,16 +36,9 @@ export default class DownloadUrlListItem extends StatefulVue {
|
||||
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
|
||||
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
return context.app.info.getDownloadUrl(os);
|
||||
const context = await ApplicationFactory.Current.getAppAsync();
|
||||
return context.info.getDownloadUrl(os);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,25 +31,27 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component
|
||||
export default class PrivacyPolicy extends StatefulVue {
|
||||
export default class PrivacyPolicy extends Vue {
|
||||
public repositoryUrl: string = '';
|
||||
public feedbackUrl: string = '';
|
||||
public isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.initialize(app);
|
||||
}
|
||||
|
||||
private initialize(app: IApplication) {
|
||||
const info = app.info;
|
||||
this.repositoryUrl = info.repositoryWebUrl;
|
||||
this.feedbackUrl = info.feedbackUrl;
|
||||
}
|
||||
protected handleCollectionState(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -47,22 +47,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
import DownloadUrlList from './DownloadUrlList.vue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PrivacyPolicy, DownloadUrlList,
|
||||
},
|
||||
})
|
||||
export default class TheFooter extends StatefulVue {
|
||||
export default class TheFooter extends Vue {
|
||||
public readonly modalName = 'privacy-policy';
|
||||
public readonly isDesktop: boolean;
|
||||
public readonly isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
|
||||
public version: string = '';
|
||||
public repositoryUrl: string = '';
|
||||
@@ -70,12 +69,12 @@ export default class TheFooter extends StatefulVue {
|
||||
public feedbackUrl: string = '';
|
||||
public homepageUrl: string = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.initialize(app);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
private initialize(app: IApplication) {
|
||||
const info = app.info;
|
||||
this.version = info.version;
|
||||
this.homepageUrl = info.homepage;
|
||||
@@ -83,10 +82,6 @@ export default class TheFooter extends StatefulVue {
|
||||
this.releaseUrl = info.releaseUrl;
|
||||
this.feedbackUrl = info.feedbackUrl;
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -104,13 +99,13 @@ export default class TheFooter extends StatefulVue {
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (max-width: $big-screen-width) {
|
||||
@media screen and (max-width: $big-screen-width) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
&__section {
|
||||
display: flex;
|
||||
@media (max-width: $big-screen-width) {
|
||||
@media screen and (max-width: $big-screen-width) {
|
||||
justify-content: space-around;
|
||||
width:100%;
|
||||
&:not(:first-child) {
|
||||
@@ -134,7 +129,7 @@ export default class TheFooter extends StatefulVue {
|
||||
content: "|";
|
||||
padding: 0 5px;
|
||||
}
|
||||
@media (max-width: $big-screen-width) {
|
||||
@media screen and (max-width: $big-screen-width) {
|
||||
margin-top: 3px;
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<h1 class="child title" >{{ title }}</h1>
|
||||
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2>
|
||||
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { ApplicationFactory } from '@/application/ApplicationFactory';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class TheHeader extends StatefulVue {
|
||||
export default class TheHeader extends Vue {
|
||||
public title = '';
|
||||
public subtitle = '';
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
public async created() {
|
||||
const app = await ApplicationFactory.Current.getAppAsync();
|
||||
this.title = app.info.name;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@ import { StatefulVue } from './StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component( {
|
||||
directives: { NonCollapsing },
|
||||
@@ -27,8 +25,6 @@ export default class TheSearchBar extends StatefulVue {
|
||||
public searchPlaceHolder = 'Search';
|
||||
public searchQuery = '';
|
||||
|
||||
private readonly listeners = new Array<IEventSubscription>();
|
||||
|
||||
@Watch('searchQuery')
|
||||
public async updateFilterAsync(newFilter: |string) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
@@ -39,28 +35,18 @@ export default class TheSearchBar extends StatefulVue {
|
||||
filter.setFilter(newFilter);
|
||||
}
|
||||
}
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) {
|
||||
const totalScripts = newState.collection.totalScripts;
|
||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
||||
this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : '';
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState.filter);
|
||||
this.events.unsubscribeAll();
|
||||
this.subscribeFilter(newState.filter);
|
||||
}
|
||||
|
||||
private subscribe(filter: IUserFilter) {
|
||||
this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result)));
|
||||
this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved()));
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
private subscribeFilter(filter: IUserFilter) {
|
||||
this.events.register(filter.filtered.on((result) => this.handleFiltered(result)));
|
||||
this.events.register(filter.filterRemoved.on(() => this.handleFilterRemoved()));
|
||||
}
|
||||
private handleFiltered(result: IFilterResult) {
|
||||
this.searchQuery = result.query;
|
||||
|
||||
30
src/presentation/Throttle.ts
Normal file
30
src/presentation/Throttle.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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,3 +1,5 @@
|
||||
$big-screen-width: 992px;
|
||||
$medium-screen-width: 768px;
|
||||
$small-screen-width: 380px;
|
||||
|
||||
$vertical-view-breakpoint: 992px;
|
||||
60
tests/unit/application/ApplicationFactory.spec.ts
Normal file
60
tests/unit/application/ApplicationFactory.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ApplicationFactory, ApplicationGetter } from '@/application/ApplicationFactory';
|
||||
import { ApplicationStub } from '../stubs/ApplicationStub';
|
||||
|
||||
describe('ApplicationFactory', () => {
|
||||
describe('ctor', () => {
|
||||
it('throws if getter is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined getter';
|
||||
const getter = undefined;
|
||||
// act
|
||||
const act = () => new SystemUnderTest(getter);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
describe('getAppAsync', () => {
|
||||
it('returns result from the getter', async () => {
|
||||
// arrange
|
||||
const expected = new ApplicationStub();
|
||||
const getter: ApplicationGetter = () => expected;
|
||||
const sut = new SystemUnderTest(getter);
|
||||
// act
|
||||
const actual = await Promise.all( [
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
]);
|
||||
// assert
|
||||
expect(actual.every((value) => value === expected));
|
||||
});
|
||||
it('only executes getter once', async () => {
|
||||
// arrange
|
||||
let totalExecution = 0;
|
||||
const expected = new ApplicationStub();
|
||||
const getter: ApplicationGetter = () => {
|
||||
totalExecution++;
|
||||
return expected;
|
||||
};
|
||||
const sut = new SystemUnderTest(getter);
|
||||
// act
|
||||
await Promise.all( [
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
sut.getAppAsync(),
|
||||
]);
|
||||
// assert
|
||||
expect(totalExecution).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class SystemUnderTest extends ApplicationFactory {
|
||||
public constructor(costlyGetter: ApplicationGetter) {
|
||||
super(costlyGetter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
|
||||
import { EnvironmentStub } from '../../stubs/EnvironmentStub';
|
||||
import { ApplicationStub } from '../../stubs/ApplicationStub';
|
||||
import { buildContextAsync } from '@/application/Context/ApplicationContextFactory';
|
||||
import { IApplicationFactory } from '@/application/IApplicationFactory';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
describe('ApplicationContextFactory', () => {
|
||||
describe('buildContextAsync', () => {
|
||||
describe('factory', () => {
|
||||
it('sets application from factory', async () => {
|
||||
// arrange
|
||||
const expected = new ApplicationStub().withCollection(
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.macOS));
|
||||
const factoryMock = mockFactoryWithApp(expected);
|
||||
// act
|
||||
const context = await buildContextAsync(factoryMock);
|
||||
// assert
|
||||
expect(expected).to.equal(context.app);
|
||||
});
|
||||
});
|
||||
describe('environment', () => {
|
||||
describe('sets initial OS as expected', () => {
|
||||
it('returns currentOs if it is supported', async () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const environment = new EnvironmentStub().withOs(expected);
|
||||
const collection = new CategoryCollectionStub().withOs(expected);
|
||||
const factoryMock = mockFactoryWithCollection(collection);
|
||||
// act
|
||||
const context = await buildContextAsync(factoryMock, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to other os if OS in environment is not supported', async () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const currentOs = OperatingSystem.macOS;
|
||||
const environment = new EnvironmentStub().withOs(currentOs);
|
||||
const collection = new CategoryCollectionStub().withOs(expected);
|
||||
const factoryMock = mockFactoryWithCollection(collection);
|
||||
// act
|
||||
const context = await buildContextAsync(factoryMock, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to most supported os if current os is not supported', async () => {
|
||||
// arrange
|
||||
const expectedOs = OperatingSystem.Android;
|
||||
const allCollections = [
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
|
||||
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
|
||||
];
|
||||
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
|
||||
const app = new ApplicationStub().withCollections(...allCollections);
|
||||
const factoryMock = mockFactoryWithApp(app);
|
||||
// act
|
||||
const context = await buildContextAsync(factoryMock, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockFactoryWithCollection(result: ICategoryCollection): IApplicationFactory {
|
||||
return mockFactoryWithApp(new ApplicationStub().withCollection(result));
|
||||
}
|
||||
|
||||
function mockFactoryWithApp(app: IApplication): IApplicationFactory {
|
||||
return {
|
||||
getAppAsync: () => Promise.resolve(app),
|
||||
};
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { ApplicationParserType, buildContext } from '@/application/Context/ApplicationContextProvider';
|
||||
import { CategoryCollectionStub } from '../../stubs/CategoryCollectionStub';
|
||||
import { EnvironmentStub } from '../../stubs/EnvironmentStub';
|
||||
import { ApplicationStub } from '../../stubs/ApplicationStub';
|
||||
|
||||
describe('ApplicationContextProvider', () => {
|
||||
describe('buildContext', () => {
|
||||
it('sets application from parser', () => {
|
||||
// arrange
|
||||
const expected = new ApplicationStub().withCollection(
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.macOS));
|
||||
const parserMock: ApplicationParserType = () => expected;
|
||||
// act
|
||||
const context = buildContext(parserMock);
|
||||
// assert
|
||||
expect(expected).to.equal(context.app);
|
||||
});
|
||||
describe('sets initial OS as expected', () => {
|
||||
it('returns currentOs if it is supported', () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const environment = new EnvironmentStub().withOs(expected);
|
||||
const parser = mockParser(new CategoryCollectionStub().withOs(expected));
|
||||
// act
|
||||
const context = buildContext(parser, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to other os if OS in environment is not supported', () => {
|
||||
// arrange
|
||||
const expected = OperatingSystem.Windows;
|
||||
const currentOs = OperatingSystem.macOS;
|
||||
const environment = new EnvironmentStub().withOs(currentOs);
|
||||
const parser = mockParser(new CategoryCollectionStub().withOs(expected));
|
||||
// act
|
||||
const context = buildContext(parser, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expected).to.equal(actual);
|
||||
});
|
||||
it('fallbacks to most supported os if current os is not supported', () => {
|
||||
// arrange
|
||||
const expectedOs = OperatingSystem.Android;
|
||||
const allCollections = [
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Linux).withTotalScripts(3),
|
||||
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
|
||||
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
|
||||
];
|
||||
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
|
||||
const app = new ApplicationStub().withCollections(...allCollections);
|
||||
const parser: ApplicationParserType = () => app;
|
||||
// act
|
||||
const context = buildContext(parser, environment);
|
||||
// assert
|
||||
const actual = context.state.os;
|
||||
expect(expectedOs).to.equal(actual, `Expected: ${OperatingSystem[expectedOs]}, actual: ${OperatingSystem[actual]}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockParser(result: ICategoryCollection): ApplicationParserType {
|
||||
return () => new ApplicationStub().withCollection(result);
|
||||
}
|
||||
@@ -23,15 +23,37 @@ describe('BatchBuilder', () => {
|
||||
});
|
||||
});
|
||||
describe('writeStandardOut', () => {
|
||||
it('prepends expected', () => {
|
||||
const testData = [
|
||||
{
|
||||
name: 'plain text',
|
||||
text: 'test',
|
||||
expected: 'echo test',
|
||||
},
|
||||
{
|
||||
name: 'text with ampersand',
|
||||
text: 'a & b',
|
||||
expected: 'echo a ^& b',
|
||||
},
|
||||
{
|
||||
name: 'text with percent sign',
|
||||
text: '90%',
|
||||
expected: 'echo 90%%',
|
||||
},
|
||||
{
|
||||
name: 'text with multiple ampersands and percent signs',
|
||||
text: 'Me&you in % ? You & me = 0%',
|
||||
expected: 'echo Me^&you in %% ? You ^& me = 0%%',
|
||||
},
|
||||
];
|
||||
for (const test of testData) {
|
||||
it(test.name, () => {
|
||||
// arrange
|
||||
const text = 'test';
|
||||
const expected = `echo ${text}`;
|
||||
const sut = new BatchBuilderRevealer();
|
||||
// act
|
||||
const actual = sut.writeStandardOut(text);
|
||||
const actual = sut.writeStandardOut(test.text);
|
||||
// assert
|
||||
expect(expected).to.equal(actual);
|
||||
expect(test.expected).to.equal(actual);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,15 +23,32 @@ describe('ShellBuilder', () => {
|
||||
});
|
||||
});
|
||||
describe('writeStandardOut', () => {
|
||||
it('prepends expected', () => {
|
||||
const testData = [
|
||||
{
|
||||
name: 'plain text',
|
||||
text: 'test',
|
||||
expected: 'echo \'test\'',
|
||||
},
|
||||
{
|
||||
name: 'text with single quote',
|
||||
text: 'I\'m not who you think I am',
|
||||
expected: 'echo \'I\'\\\'\'m not who you think I am\'',
|
||||
},
|
||||
{
|
||||
name: 'text with multiple single quotes',
|
||||
text: 'I\'m what you\'re',
|
||||
expected: 'echo \'I\'\\\'\'m what you\'\\\'\'re\'',
|
||||
},
|
||||
];
|
||||
for (const test of testData) {
|
||||
it(test.name, () => {
|
||||
// arrange
|
||||
const text = 'test';
|
||||
const expected = `echo '${text}'`;
|
||||
const sut = new ShellBuilderRevealer();
|
||||
// act
|
||||
const actual = sut.writeStandardOut(text);
|
||||
const actual = sut.writeStandardOut(test.text);
|
||||
// assert
|
||||
expect(expected).to.equal(actual);
|
||||
expect(test.expected).to.equal(actual);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,10 @@ import { mockEnumParser } from '../../stubs/EnumParserStub';
|
||||
import { ProjectInformationStub } from '../../stubs/ProjectInformationStub';
|
||||
import { getCategoryStub, CollectionDataStub } from '../../stubs/CollectionDataStub';
|
||||
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
||||
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
|
||||
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
||||
import { FunctionDataStub } from '../../stubs/FunctionDataStub';
|
||||
import { RecommendationLevel } from '../../../../src/domain/RecommendationLevel';
|
||||
|
||||
describe('CategoryCollectionParser', () => {
|
||||
describe('parseCategoryCollection', () => {
|
||||
@@ -93,5 +97,35 @@ describe('CategoryCollectionParser', () => {
|
||||
expect(actual.os).to.equal(expectedOs);
|
||||
});
|
||||
});
|
||||
describe('functions', () => {
|
||||
it('compiles script call with given function', () => {
|
||||
// arrange
|
||||
const expectedCode = 'code-from-the-function';
|
||||
const functionName = 'function-name';
|
||||
const scriptName = 'script-name';
|
||||
const script = ScriptDataStub.createWithCall({ function: functionName })
|
||||
.withName(scriptName);
|
||||
const func = FunctionDataStub.createWithCode()
|
||||
.withName(functionName)
|
||||
.withCode(expectedCode);
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([ script,
|
||||
ScriptDataStub.createWithCode().withName('2')
|
||||
.withRecommendationLevel(RecommendationLevel.Standard),
|
||||
ScriptDataStub.createWithCode()
|
||||
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
|
||||
]);
|
||||
const collection = new CollectionDataStub()
|
||||
.withActions([ category ])
|
||||
.withFunctions([ func ]);
|
||||
const info = new ProjectInformationStub();
|
||||
// act
|
||||
const actual = parseCategoryCollection(collection, info);
|
||||
// assert
|
||||
const actualScript = actual.findScript(scriptName);
|
||||
const actualCode = actualScript.code.execute;
|
||||
expect(actualCode).to.equal(expectedCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseCategory } from '@/application/Parser/CategoryParser';
|
||||
import { CategoryData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
||||
import { parseScript } from '@/application/Parser/Script/ScriptParser';
|
||||
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
|
||||
import { ScriptCompilerStub } from '../../stubs/ScriptCompilerStub';
|
||||
import { ScriptDataStub } from '../../stubs/ScriptDataStub';
|
||||
import { CategoryCollectionParseContextStub } from '../../stubs/CategoryCollectionParseContextStub';
|
||||
import { LanguageSyntaxStub } from '../../stubs/LanguageSyntaxStub';
|
||||
import { CategoryDataStub } from '../../stubs/CategoryDataStub';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
@@ -26,10 +26,9 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const categoryName = 'test';
|
||||
const expectedMessage = `category has no children: "${categoryName}"`;
|
||||
const category: CategoryData = {
|
||||
category: categoryName,
|
||||
children: [],
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withName(categoryName)
|
||||
.withChildren([]);
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const act = () => parseCategory(category, context);
|
||||
@@ -40,10 +39,9 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const categoryName = 'test';
|
||||
const expectedMessage = `category has no children: "${categoryName}"`;
|
||||
const category: CategoryData = {
|
||||
category: categoryName,
|
||||
children: undefined,
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withName(categoryName)
|
||||
.withChildren(undefined);
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const act = () => parseCategory(category, context);
|
||||
@@ -55,10 +53,8 @@ describe('CategoryParser', () => {
|
||||
const expectedMessage = 'category has no name';
|
||||
const invalidNames = ['', undefined];
|
||||
invalidNames.forEach((invalidName) => {
|
||||
const category: CategoryData = {
|
||||
category: invalidName,
|
||||
children: getTestChildren(),
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withName(invalidName);
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const act = () => parseCategory(category, context);
|
||||
@@ -71,7 +67,7 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined context';
|
||||
const context = undefined;
|
||||
const category = getValidCategory();
|
||||
const category = new CategoryDataStub();
|
||||
// act
|
||||
const act = () => parseCategory(category, context);
|
||||
// assert
|
||||
@@ -81,11 +77,8 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const expected = parseDocUrls({ docs: url });
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: getTestChildren(),
|
||||
docs: url,
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withDocs(url);
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const actual = parseCategory(category, context).documentationUrls;
|
||||
@@ -98,10 +91,8 @@ describe('CategoryParser', () => {
|
||||
const script = ScriptDataStub.createWithCode();
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
const expected = [ parseScript(script, context) ];
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: [ script ],
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([ script ]);
|
||||
// act
|
||||
const actual = parseCategory(category, context).scripts;
|
||||
// assert
|
||||
@@ -115,10 +106,8 @@ describe('CategoryParser', () => {
|
||||
const context = new CategoryCollectionParseContextStub()
|
||||
.withCompiler(compiler);
|
||||
const expected = [ parseScript(script, context) ];
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: [ script ],
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([ script ]);
|
||||
// act
|
||||
const actual = parseCategory(category, context).scripts;
|
||||
// assert
|
||||
@@ -128,10 +117,8 @@ describe('CategoryParser', () => {
|
||||
// arrange
|
||||
const callableScript = ScriptDataStub.createWithCall();
|
||||
const scripts = [ callableScript, ScriptDataStub.createWithCode() ];
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: scripts,
|
||||
};
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren(scripts);
|
||||
const compiler = new ScriptCompilerStub()
|
||||
.withCompileAbility(callableScript);
|
||||
const context = new CategoryCollectionParseContextStub()
|
||||
@@ -148,19 +135,16 @@ describe('CategoryParser', () => {
|
||||
const duplicatedCode = `${commentDelimiter} duplicate-line\n${commentDelimiter} duplicate-line`;
|
||||
const parseContext = new CategoryCollectionParseContextStub()
|
||||
.withSyntax(new LanguageSyntaxStub().withCommentDelimiters(commentDelimiter));
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: [
|
||||
{
|
||||
category: 'sub-category',
|
||||
children: [
|
||||
const category = new CategoryDataStub()
|
||||
.withChildren([
|
||||
new CategoryDataStub()
|
||||
.withName('sub-category')
|
||||
.withChildren([
|
||||
ScriptDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
.withCode(duplicatedCode),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
]),
|
||||
]);
|
||||
// act
|
||||
const act = () => parseCategory(category, parseContext).scripts;
|
||||
// assert
|
||||
@@ -169,14 +153,13 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
it('returns expected subcategories', () => {
|
||||
// arrange
|
||||
const expected: CategoryData[] = [ {
|
||||
category: 'test category',
|
||||
children: [ ScriptDataStub.createWithCode() ],
|
||||
}];
|
||||
const category: CategoryData = {
|
||||
category: 'category name',
|
||||
children: expected,
|
||||
};
|
||||
const expected = [ new CategoryDataStub()
|
||||
.withName('test category')
|
||||
.withChildren([ ScriptDataStub.createWithCode() ]),
|
||||
];
|
||||
const category = new CategoryDataStub()
|
||||
.withName('category name')
|
||||
.withChildren(expected);
|
||||
const context = new CategoryCollectionParseContextStub();
|
||||
// act
|
||||
const actual = parseCategory(category, context).subCategories;
|
||||
@@ -187,17 +170,3 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getValidCategory(): CategoryData {
|
||||
return {
|
||||
category: 'category name',
|
||||
children: getTestChildren(),
|
||||
docs: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getTestChildren(): ReadonlyArray<CategoryOrScriptData> {
|
||||
return [
|
||||
ScriptDataStub.createWithCode(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('CategoryCollectionParseContext', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined scripting';
|
||||
const scripting = undefined;
|
||||
const functionsData = [ new FunctionDataStub() ];
|
||||
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||
// act
|
||||
const act = () => new CategoryCollectionParseContext(functionsData, scripting);
|
||||
// assert
|
||||
@@ -39,7 +39,7 @@ describe('CategoryCollectionParseContext', () => {
|
||||
describe('compiler', () => {
|
||||
it('constructed as expected', () => {
|
||||
// arrange
|
||||
const functionsData = [ new FunctionDataStub() ];
|
||||
const functionsData = [ FunctionDataStub.createWithCode() ];
|
||||
const syntax = new LanguageSyntaxStub();
|
||||
const expected = new ScriptCompiler(functionsData, syntax);
|
||||
const language = ScriptingLanguage.shellscript;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
|
||||
describe('ExpressionsCompiler', () => {
|
||||
describe('parameter substitution', () => {
|
||||
describe('substitutes as expected', () => {
|
||||
// arrange
|
||||
const testCases = [ {
|
||||
name: 'with different parameters',
|
||||
code: 'He{{ $firstParameter }} {{ $secondParameter }}!',
|
||||
parameters: {
|
||||
firstParameter: 'llo',
|
||||
secondParameter: 'world',
|
||||
},
|
||||
expected: 'Hello world!',
|
||||
}, {
|
||||
name: 'with single parameter',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: 'Hodor',
|
||||
},
|
||||
expected: 'Hodor!',
|
||||
|
||||
}];
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
const sut = new MockableExpressionsCompiler();
|
||||
// act
|
||||
const actual = sut.compileExpressions(testCase.code, testCase.parameters);
|
||||
// assert
|
||||
expect(actual).to.equal(testCase.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
describe('throws when expected value is not provided', () => {
|
||||
// arrange
|
||||
const noParameterTestCases = [
|
||||
{
|
||||
name: 'empty parameters',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'undefined parameters',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: undefined,
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'unnecessary parameter provided',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
unnecessaryParameter: 'unnecessaryValue',
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'undefined value',
|
||||
code: '{{ $parameter }}!',
|
||||
parameters: {
|
||||
parameter: undefined,
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter"',
|
||||
},
|
||||
{
|
||||
name: 'multiple values are not',
|
||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||
parameters: {},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2", "parameter3"',
|
||||
},
|
||||
{
|
||||
name: 'some values are provided',
|
||||
code: '{{ $parameter1 }}, {{ $parameter2 }}, {{ $parameter3 }}',
|
||||
parameters: {
|
||||
parameter2: 'value',
|
||||
},
|
||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3"',
|
||||
},
|
||||
];
|
||||
for (const testCase of noParameterTestCases) {
|
||||
it(testCase.name, () => {
|
||||
const sut = new MockableExpressionsCompiler();
|
||||
// act
|
||||
const act = () => sut.compileExpressions(testCase.code, testCase.parameters);
|
||||
// assert
|
||||
expect(act).to.throw(testCase.expectedError);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class MockableExpressionsCompiler extends ExpressionsCompiler {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { generateIlCode } from '@/application/Parser/Script/Compiler/ILCode';
|
||||
import { generateIlCode } from '@/application/Parser/Script/Compiler/Expressions/ILCode';
|
||||
|
||||
describe('ILCode', () => {
|
||||
describe('getUniqueParameterNames', () => {
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
|
||||
import { FunctionCompiler } from '@/application/Parser/Script/Compiler/Function/FunctionCompiler';
|
||||
import { FunctionCallCompilerStub } from '../../../../../stubs/FunctionCallCompilerStub';
|
||||
import { FunctionDataStub } from '../../../../../stubs/FunctionDataStub';
|
||||
|
||||
describe('FunctionsCompiler', () => {
|
||||
describe('compileFunctions', () => {
|
||||
describe('validates functions', () => {
|
||||
it('throws if one of the functions is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = `some functions are undefined`;
|
||||
const functions = [ FunctionDataStub.createWithCode(), undefined ];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when functions have same names', () => {
|
||||
// arrange
|
||||
const name = 'same-func-name';
|
||||
const expectedError = `duplicate function name: "${name}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
FunctionDataStub.createWithCode().withName(name),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws when function parameters have same names', () => {
|
||||
// arrange
|
||||
const parameterName = 'duplicate-parameter';
|
||||
const func = FunctionDataStub.createWithCall()
|
||||
.withParameters(parameterName, parameterName);
|
||||
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ func ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
describe('throws when when function have duplicate code', () => {
|
||||
it('code', () => {
|
||||
// arrange
|
||||
const code = 'duplicate-code';
|
||||
const expectedError = `duplicate "code" in functions: "${code}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code),
|
||||
FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('revertCode', () => {
|
||||
// arrange
|
||||
const revertCode = 'duplicate-revert-code';
|
||||
const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`;
|
||||
const functions = [
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('func-1').withCode('code-1').withRevertCode(revertCode),
|
||||
FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('func-2').withCode('code-2').withRevertCode(revertCode),
|
||||
];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions(functions);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('both code and call are defined', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `both "code" and "call" are defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(functionName)
|
||||
.withCode('code')
|
||||
.withMockCall();
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('neither code and call is defined', () => {
|
||||
// arrange
|
||||
const functionName = 'invalid-function';
|
||||
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
|
||||
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(functionName);
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const act = () => sut.compileFunctions([ invalidFunction ]);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('returns empty with empty functions', () => {
|
||||
// arrange
|
||||
const emptyValues = [ [], undefined ];
|
||||
const sut = new MockableFunctionCompiler();
|
||||
for (const emptyFunctions of emptyValues) {
|
||||
// act
|
||||
const actual = sut.compileFunctions(emptyFunctions);
|
||||
// assert
|
||||
expect(actual).to.not.equal(undefined);
|
||||
}
|
||||
});
|
||||
it('parses single function with code as expected', () => {
|
||||
// arrange
|
||||
const name = 'function-name';
|
||||
const expected = FunctionDataStub
|
||||
.createWithoutCallOrCodes()
|
||||
.withName(name)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code')
|
||||
.withParameters('expected-parameter-1', 'expected-parameter-2');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ expected ]);
|
||||
// expect
|
||||
const actual = collection.getFunctionByName(name);
|
||||
expectEqualFunctions(expected, actual);
|
||||
});
|
||||
it('parses function with call as expected', () => {
|
||||
// arrange
|
||||
const calleeName = 'callee-function';
|
||||
const caller = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function')
|
||||
.withCall({ function: calleeName });
|
||||
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(calleeName)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ caller, callee ]);
|
||||
// expect
|
||||
const actual = collection.getFunctionByName(caller.name);
|
||||
expectEqualFunctionCode(callee, actual);
|
||||
});
|
||||
it('parses multiple functions with call as expected', () => {
|
||||
// arrange
|
||||
const calleeName = 'callee-function';
|
||||
const caller1 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function')
|
||||
.withCall({ function: calleeName });
|
||||
const caller2 = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName('caller-function-2')
|
||||
.withCall({ function: calleeName });
|
||||
const callee = FunctionDataStub.createWithoutCallOrCodes()
|
||||
.withName(calleeName)
|
||||
.withCode('expected-code')
|
||||
.withRevertCode('expected-revert-code');
|
||||
const sut = new MockableFunctionCompiler();
|
||||
// act
|
||||
const collection = sut.compileFunctions([ caller1, caller2, callee ]);
|
||||
// expect
|
||||
const compiledCaller1 = collection.getFunctionByName(caller1.name);
|
||||
const compiledCaller2 = collection.getFunctionByName(caller2.name);
|
||||
expectEqualFunctionCode(callee, compiledCaller1);
|
||||
expectEqualFunctionCode(callee, compiledCaller2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expectEqualFunctions(expected: FunctionData, actual: ISharedFunction) {
|
||||
expect(actual.name).to.equal(expected.name);
|
||||
expect(actual.parameters).to.deep.equal(expected.parameters);
|
||||
expectEqualFunctionCode(expected, actual);
|
||||
}
|
||||
|
||||
function expectEqualFunctionCode(expected: FunctionData, actual: ISharedFunction) {
|
||||
expect(actual.code).to.equal(expected.code);
|
||||
expect(actual.revertCode).to.equal(expected.revertCode);
|
||||
}
|
||||
|
||||
class MockableFunctionCompiler extends FunctionCompiler {
|
||||
constructor(functionCallCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub()) {
|
||||
super(functionCallCompiler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SharedFunction } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
|
||||
|
||||
describe('SharedFunction', () => {
|
||||
describe('name', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-function-name';
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withName(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.name).equal(expected);
|
||||
});
|
||||
it('throws if empty or undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function name';
|
||||
const invalidValues = [ undefined, '' ];
|
||||
for (const invalidValue of invalidValues) {
|
||||
// act
|
||||
const act = () => new SharedFunctionBuilder()
|
||||
.withName(invalidValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('parameters', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = [ 'expected-parameter' ];
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withParameters(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.parameters).to.deep.equal(expected);
|
||||
});
|
||||
it('returns empty array if undefined', () => {
|
||||
// arrange
|
||||
const expected = [ ];
|
||||
const value = undefined;
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withParameters(value)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.parameters).to.not.equal(undefined);
|
||||
expect(sut.parameters).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
describe('code', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = 'expected-code';
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withCode(expected)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.code).equal(expected);
|
||||
});
|
||||
it('throws if empty or undefined', () => {
|
||||
// arrange
|
||||
const functionName = 'expected-function-name';
|
||||
const expectedError = `undefined function ("${functionName}") code`;
|
||||
const invalidValues = [ undefined, '' ];
|
||||
for (const invalidValue of invalidValues) {
|
||||
// act
|
||||
const act = () => new SharedFunctionBuilder()
|
||||
.withName(functionName)
|
||||
.withCode(invalidValue)
|
||||
.build();
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('revertCode', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const testData = [ 'expected-revert-code', undefined, '' ];
|
||||
for (const data of testData) {
|
||||
// act
|
||||
const sut = new SharedFunctionBuilder()
|
||||
.withRevertCode(data)
|
||||
.build();
|
||||
// assert
|
||||
expect(sut.revertCode).equal(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class SharedFunctionBuilder {
|
||||
private name = 'name';
|
||||
private parameters: readonly string[] = [ 'parameter' ];
|
||||
private code = 'code';
|
||||
private revertCode = 'revert-code';
|
||||
|
||||
public build(): SharedFunction {
|
||||
return new SharedFunction(
|
||||
this.name,
|
||||
this.parameters,
|
||||
this.code,
|
||||
this.revertCode,
|
||||
);
|
||||
}
|
||||
public withName(name: string) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
public withParameters(parameters: readonly string[]) {
|
||||
this.parameters = parameters;
|
||||
return this;
|
||||
}
|
||||
public withCode(code: string) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
public withRevertCode(revertCode: string) {
|
||||
this.revertCode = revertCode;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection';
|
||||
import { SharedFunctionStub } from '../../../../../stubs/SharedFunctionStub';
|
||||
|
||||
describe('SharedFunctionCollection', () => {
|
||||
describe('addFunction', () => {
|
||||
it('throws if function is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function';
|
||||
const func = undefined;
|
||||
const sut = new SharedFunctionCollection();
|
||||
// act
|
||||
const act = () => sut.addFunction(func);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('throws if function with same name already exists', () => {
|
||||
// arrange
|
||||
const functionName = 'duplicate-function';
|
||||
const expectedError = `function with name ${functionName} already exists`;
|
||||
const func = new SharedFunctionStub()
|
||||
.withName('duplicate-function');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
// act
|
||||
const act = () => sut.addFunction(func);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
|
||||
});
|
||||
});
|
||||
describe('getFunctionByName', () => {
|
||||
it('throws if name is undefined', () => {
|
||||
// arrange
|
||||
const expectedError = 'undefined function name';
|
||||
const invalidValues = [ undefined, '' ];
|
||||
const sut = new SharedFunctionCollection();
|
||||
for (const invalidValue of invalidValues) {
|
||||
const name = invalidValue;
|
||||
// act
|
||||
const act = () => sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}
|
||||
});
|
||||
it('throws if function does not exist', () => {
|
||||
// arrange
|
||||
const name = 'unique-name';
|
||||
const expectedError = `called function is not defined "${name}"`;
|
||||
const func = new SharedFunctionStub()
|
||||
.withName('unexpected-name');
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(func);
|
||||
// act
|
||||
const act = () => sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
it('returns existing function', () => {
|
||||
// arrange
|
||||
const name = 'expected-function-name';
|
||||
const expected = new SharedFunctionStub()
|
||||
.withName(name);
|
||||
const sut = new SharedFunctionCollection();
|
||||
sut.addFunction(new SharedFunctionStub().withName('another-function-name'));
|
||||
sut.addFunction(expected);
|
||||
// act
|
||||
const actual = sut.getFunctionByName(name);
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user