Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
504fa056d7 | ||
|
|
739287ac71 | ||
|
|
ab8bce7686 | ||
|
|
e6152fa76f | ||
|
|
a8031d18d5 | ||
|
|
9aa8166891 | ||
|
|
236a0f6c82 | ||
|
|
2492f2d814 | ||
|
|
410bcd8244 | ||
|
|
b08a6b5cec | ||
|
|
37ad26a082 | ||
|
|
0696ed8396 | ||
|
|
9942df16c8 | ||
|
|
20b7d283b0 | ||
|
|
f39ee76c0c | ||
|
|
4b2390736a | ||
|
|
c8cb7a5c28 | ||
|
|
5217b0b758 | ||
|
|
ddf417a16a | ||
|
|
2f0321f315 | ||
|
|
4d7ff7edc5 | ||
|
|
862914b06e | ||
|
|
6c3c2e6709 | ||
|
|
c92dc1e253 | ||
|
|
e73c0ad1bf | ||
|
|
6a89c6224b | ||
|
|
dcccb61781 | ||
|
|
c0c475ff56 | ||
|
|
6dc768817f | ||
|
|
439cd303ff | ||
|
|
ec0c972d34 | ||
|
|
2a08855e5d | ||
|
|
1c6b3057ea | ||
|
|
ea5f9ec27d | ||
|
|
f2935e4008 | ||
|
|
487001af48 | ||
|
|
71e70e50c5 | ||
|
|
0a857aa09e | ||
|
|
b976b92031 | ||
|
|
db62ed7f3a | ||
|
|
36f0805590 | ||
|
|
49600c5f37 | ||
|
|
eb9ac35a92 | ||
|
|
77148980e0 | ||
|
|
b3d2e82025 | ||
|
|
b25b8cc805 | ||
|
|
8141a01ef7 | ||
|
|
a2f10857e2 | ||
|
|
aea04e5f7c | ||
|
|
60c80611ea | ||
|
|
b1ed3ce55f | ||
|
|
040ed2701c | ||
|
|
00d8e551db | ||
|
|
3e9c99f5f8 | ||
|
|
02bdc4cf04 | ||
|
|
5c43965f0b | ||
|
|
b2376ecc30 | ||
|
|
aeaa6deeb4 | ||
|
|
448e378dc4 | ||
|
|
ac2249f256 | ||
|
|
05932c5a36 | ||
|
|
6f46cdb4ed | ||
|
|
5f527a00cf | ||
|
|
1935db1019 | ||
|
|
1f515e7be5 | ||
|
|
1a5f92021f | ||
|
|
f3c7413f52 | ||
|
|
646db90585 | ||
|
|
1f8a0cf9ab | ||
|
|
bd41af466f | ||
|
|
970221b996 | ||
|
|
15004ff1f1 | ||
|
|
65226f3984 | ||
|
|
b0a7d0b53b | ||
|
|
ee43fd92a0 | ||
|
|
cf39e6d254 | ||
|
|
1260eea690 | ||
|
|
45a3669443 | ||
|
|
c9b91f6d8f | ||
|
|
9a6b903b92 | ||
|
|
7661575573 | ||
|
|
f1abd7682f |
32
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
32
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -11,9 +11,11 @@ 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.
|
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
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
<!--
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
### OS
|
### OS
|
||||||
|
|
||||||
@@ -23,14 +25,32 @@ 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".
|
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Screenshots
|
### Reproduction steps
|
||||||
|
|
||||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
<!--
|
||||||
|
How can the bug be recreated?
|
||||||
|
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||||
|
E.g.
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
-->
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
<!-- Which scripts did you execute? If applicable, please paste the executed scripts or attach the generated privacy.sexy file . -->
|
<!--
|
||||||
|
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Screenshots
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
-->
|
||||||
|
|
||||||
### Additional information
|
### Additional information
|
||||||
|
|
||||||
<!-- Add any other context about the problem here. -->
|
<!--
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
-->
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
17
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -11,13 +11,16 @@ 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.
|
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
|
### Description
|
||||||
|
|
||||||
<!-- A clear and concise description of what the bug is. -->
|
<!--
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
-->
|
||||||
|
|
||||||
### To Reproduce
|
### Reproduction steps
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -41,12 +44,12 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
||||||
- Web (on Desktop or mobile?)
|
- Web (on Desktop or mobile?)
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
- Or desktop (Windows, macOS or Linux?)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Additional context
|
### Additional context
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Add any other context about the problem here.
|
If applicable, add any other context about the problem here.
|
||||||
-->
|
-->
|
||||||
|
|||||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for privacy.sexy
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting an idea to improve privacy better 🤗.
|
||||||
|
Please fill in as much of the template below as you're able.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Problem description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
What are we trying to solve?
|
||||||
|
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
E.g. 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
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add any other context or screenshots about the feature request here.
|
||||||
|
-->
|
||||||
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
27
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
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. -->
|
|
||||||
73
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
73
.github/ISSUE_TEMPLATE/4-new-script-suggestion.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: New script suggestion
|
||||||
|
about: Suggest a new script for privacy.sexy
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for suggesting an script to make privacy better. 🤗
|
||||||
|
Please fill in as much of the template below as you're able.
|
||||||
|
You could alternatively send a PR directly (see CONTRIBUTING.md).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### OS
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Which OS will the new script configure?
|
||||||
|
Either "Windows" or "macOS".
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Name
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The name of the script.
|
||||||
|
It should start with an imperative noun such as "disable", "turn off" , "clear"...
|
||||||
|
E.g. "Disable webcam telemetry"
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Script code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Code that will be executed when script is selected.
|
||||||
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
|
Allowed languages:
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Revert code
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, add code that will revert the script code to its original (OS default) state.
|
||||||
|
It may require additional time, but it's much appreciated by the community.
|
||||||
|
Leave blank if the script is nonreversible (e.g. when clearing data without backup).
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested category
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest one more multiple suitable parent category of script.
|
||||||
|
A category is the item where the script will be presented under.
|
||||||
|
Most likely there already is a category for the script, so check the existing categories.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Suggested recommendation level
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, suggest recommending the script or not recommending at all.
|
||||||
|
A script should be only recommended if it'll be safe for your grandmother to run.
|
||||||
|
So you have three options here:
|
||||||
|
STANDARD: Non-breaking scripts that does not limit any functionality.
|
||||||
|
STRICT: Scripts that can break certain functionality but not intrusive to common daily OS usage.
|
||||||
|
NONE: Script is not recommended for newbies at all, only those who knows what's going on should select it.
|
||||||
|
If you're unsure, leave blank and maintainer(s) will choose one.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Additional documentation/references
|
||||||
|
|
||||||
|
<!--
|
||||||
|
If applicable, refer to documentation that should show up on the script description.
|
||||||
|
Sources (URLs) should be as high quality as possible e.g. vendor documentation is favored over user forums.
|
||||||
|
-->
|
||||||
4
.github/workflows/deploy-desktop.yaml
vendored
4
.github/workflows/deploy-desktop.yaml
vendored
@@ -22,10 +22,10 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run tests
|
- name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
- name: Publish desktop app
|
- name: Publish desktop app
|
||||||
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
|
|||||||
4
.github/workflows/deploy-site.yaml
vendored
4
.github/workflows/deploy-site.yaml
vendored
@@ -83,13 +83,13 @@ jobs:
|
|||||||
name: "App: Setup node"
|
name: "App: Setup node"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: site
|
working-directory: site
|
||||||
-
|
-
|
||||||
name: "App: Run tests"
|
name: "App: Run unit tests"
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
working-directory: site
|
working-directory: site
|
||||||
-
|
-
|
||||||
|
|||||||
3
.github/workflows/quality-checks.yaml
vendored
3
.github/workflows/quality-checks.yaml
vendored
@@ -13,13 +13,14 @@ jobs:
|
|||||||
- npm run lint:md
|
- npm run lint:md
|
||||||
- npm run lint:md:relative-urls
|
- npm run lint:md:relative-urls
|
||||||
- npm run lint:md:consistency
|
- npm run lint:md:consistency
|
||||||
|
fail-fast: false # So it continues with other commands if one fails
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 15.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Lint
|
- name: Lint
|
||||||
|
|||||||
6
.github/workflows/security-checks.yaml
vendored
6
.github/workflows/security-checks.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
npm-audit:
|
npm-audit:
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: NPM audit
|
name: NPM audit
|
||||||
run: npm audit
|
run: exit "$(npm audit)" # Since node 15.x, it does not fail with error if we don't explicitly exit
|
||||||
|
|||||||
14
.github/workflows/test.yaml
vendored
14
.github/workflows/test.yaml
vendored
@@ -1,12 +1,17 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule: # for integration tests
|
||||||
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos, ubuntu, windows]
|
os: [macos, ubuntu, windows]
|
||||||
|
fail-fast: false # So it still runs on other OSes if one of them fails
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
@@ -16,10 +21,13 @@ jobs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '14.x'
|
node-version: 15.x
|
||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
-
|
||||||
name: Run tests
|
name: Run unit tests
|
||||||
run: npm run test:unit
|
run: npm run test:unit
|
||||||
|
-
|
||||||
|
name: Run integration tests
|
||||||
|
run: npm run test:integration
|
||||||
|
|||||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -1,5 +1,87 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.10.3 (2021-08-27)
|
||||||
|
|
||||||
|
* unrecommend VSS and document its breaking behavior | [7714898](https://github.com/undergroundwires/privacy.sexy/commit/77148980e08859f89c15c6604e55b56ce4f74358)
|
||||||
|
* fix incorrect modification of Desktop folder on ThisPC (#71) | [eb9ac35](https://github.com/undergroundwires/privacy.sexy/commit/eb9ac35a923325cc2c9983ef71c0d904337a58f5)
|
||||||
|
* add initial integration tests | [49600c5](https://github.com/undergroundwires/privacy.sexy/commit/49600c5f37ca33c1687885fdf02a71ef7d3e6e8c)
|
||||||
|
* unify usage of sleepAsync and add tests | [36f0805](https://github.com/undergroundwires/privacy.sexy/commit/36f08055909f371fd9cbe3480ea813b963aea22b)
|
||||||
|
* fix broken URLs and automate broken URL checks #70 | [db62ed7](https://github.com/undergroundwires/privacy.sexy/commit/db62ed7f3ac63e9f2d762eb946060595eb9f5626)
|
||||||
|
* fix hiding recent files in quick access | [b976b92](https://github.com/undergroundwires/privacy.sexy/commit/b976b920318dba55b32d39f148fdca4f6be3cce3)
|
||||||
|
* bump dependencies to latest #75, #69 | [0a857aa](https://github.com/undergroundwires/privacy.sexy/commit/0a857aa09ee703d34ad0422bd1731158017a9a58)
|
||||||
|
* Fix NTP configuration before running the service (#72) | [71e70e5](https://github.com/undergroundwires/privacy.sexy/commit/71e70e50c51249bb10f6203414948b325acc2b2a)
|
||||||
|
* Fix typo on main page (#82) | [487001a](https://github.com/undergroundwires/privacy.sexy/commit/487001af485fdbb958615d7b52c09c2e386ddaf2)
|
||||||
|
* Improve issue templates | [f2935e4](https://github.com/undergroundwires/privacy.sexy/commit/f2935e4008f1231ef174f8932290e11715564d20)
|
||||||
|
* Fix infinitely subscribing to state changes | [ea5f9ec](https://github.com/undergroundwires/privacy.sexy/commit/ea5f9ec27df7cec6ac575e23fef18948d2b8e68a)
|
||||||
|
* Fix select options being clickable when disabled | [1c6b305](https://github.com/undergroundwires/privacy.sexy/commit/1c6b3057ea6e45125cadf374f20a905712ccdf3c)
|
||||||
|
* Fix tests for `ParameterSubstitutionParser` | [2a08855](https://github.com/undergroundwires/privacy.sexy/commit/2a08855e5d1bdf74354fd692cbfebd1a48e495ac)
|
||||||
|
* Fix excessive highlighting on hover | [ec0c972](https://github.com/undergroundwires/privacy.sexy/commit/ec0c972d348ffd5897f115d201031b704875b56a)
|
||||||
|
* Fix dead URLs | [439cd30](https://github.com/undergroundwires/privacy.sexy/commit/439cd303ff3db96a53664e5f44fefe12b95c5e6c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.2...0.10.3)
|
||||||
|
|
||||||
|
## 0.10.2 (2021-04-19)
|
||||||
|
|
||||||
|
* in CI/CD, run other tests/check even if one of them fails | [5c43965](https://github.com/undergroundwires/privacy.sexy/commit/5c43965f0bc44f991ada7d3bad68937a80665dc3)
|
||||||
|
* fix desktop initial window size being bigger than current display size on smaller Linux/Windows screens | [02bdc4c](https://github.com/undergroundwires/privacy.sexy/commit/02bdc4cf0426c452f3fc9af52b819ca9b0757290)
|
||||||
|
* refactor extra code, duplicates, complexity | [00d8e55](https://github.com/undergroundwires/privacy.sexy/commit/00d8e551db001247fadfb6f6af7a4c5ce19a9e64)
|
||||||
|
* improve disabling ads and marketing #65 | [040ed27](https://github.com/undergroundwires/privacy.sexy/commit/040ed2701c4a468749901f4c5369b221bc0973c4)
|
||||||
|
* document breaking behavior in script name #64 | [b1ed3ce](https://github.com/undergroundwires/privacy.sexy/commit/b1ed3ce55f2d003cad1ead23e674aa66d4eb5802)
|
||||||
|
* add module alias '@tests/' | [60c8061](https://github.com/undergroundwires/privacy.sexy/commit/60c80611eab227791fabb883caf93418cef5fd00)
|
||||||
|
* document chromium warning for policy changes | [aea04e5](https://github.com/undergroundwires/privacy.sexy/commit/aea04e5f7cd48fbb9b407b68ade75575a6064c82)
|
||||||
|
* fix script revert activating recommendation level | [a2f1085](https://github.com/undergroundwires/privacy.sexy/commit/a2f10857e2a8debb3ce01f79b0dfbe8649ea9a17)
|
||||||
|
* fix typo and dead URL in Windows scripts (#70) | [8141a01](https://github.com/undergroundwires/privacy.sexy/commit/8141a01ef798331b4d82f5ca95f7b18df4f6f912)
|
||||||
|
* fix vue warning for undefined property during render | [b25b8cc](https://github.com/undergroundwires/privacy.sexy/commit/b25b8cc8052655af70b0695c6c3085974d783bb6)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.1...0.10.2)
|
||||||
|
|
||||||
|
## 0.10.1 (2021-03-25)
|
||||||
|
|
||||||
|
* refactor script compilation to make it easy to add new expressions #41 #53 | [646db90](https://github.com/undergroundwires/privacy.sexy/commit/646db9058541cebd0af437554de04fdc6bb63a6e)
|
||||||
|
* restructure presentation layer | [f3c7413](https://github.com/undergroundwires/privacy.sexy/commit/f3c7413f529be4a00dba7b0ab23904b48ea13a35)
|
||||||
|
* fix a test where "it" is not used inside "describe" | [1a5f920](https://github.com/undergroundwires/privacy.sexy/commit/1a5f92021f7423cd039f8f5326cd6f99b355c962)
|
||||||
|
* bump dependencies to latest | [1f515e7](https://github.com/undergroundwires/privacy.sexy/commit/1f515e7be525291c960ccb71db05312db6da53f5)
|
||||||
|
* fix throttle function not being able to run with argument(s) | [1935db1](https://github.com/undergroundwires/privacy.sexy/commit/1935db10192051401ab00ca2cd767955d0d3b866)
|
||||||
|
* fix fs module hanging not allowing code to run | [5f527a0](https://github.com/undergroundwires/privacy.sexy/commit/5f527a00cf225d3e74b3f6577d6e2456e919de24)
|
||||||
|
* refactor all modals to use same dialog component | [6f46cdb](https://github.com/undergroundwires/privacy.sexy/commit/6f46cdb4ed49a8941c6c0dde5c5e2a816c06daef)
|
||||||
|
* fix safari cleanup scripts that are not working on modern versions | [05932c5](https://github.com/undergroundwires/privacy.sexy/commit/05932c5a36446d551c5bc811165e3295fbe15e3f)
|
||||||
|
* refactor features to use shared functions #41 | [ac2249f](https://github.com/undergroundwires/privacy.sexy/commit/ac2249f25664827d8a6d2c7ebd659ccf126b0cde)
|
||||||
|
* increase performance by polyfilling ResizeObserver only if required | [448e378](https://github.com/undergroundwires/privacy.sexy/commit/448e378dc4501f9de69af63634c87d0e5060bf52)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.10.0...0.10.1)
|
||||||
|
|
||||||
|
## 0.10.0 (2021-03-02)
|
||||||
|
|
||||||
|
* allow functions to call other functions #53 | [7661575](https://github.com/undergroundwires/privacy.sexy/commit/7661575573c6d3e8f4bc28bfa7a124a764c72ef9)
|
||||||
|
* add option to run script directly in desktop app | [9a6b903](https://github.com/undergroundwires/privacy.sexy/commit/9a6b903b9297802845043fd41115756acd4a145c)
|
||||||
|
* add script to automatically kill devicecensus process | [c9b91f6](https://github.com/undergroundwires/privacy.sexy/commit/c9b91f6d8f9bd16308b6beda119e7154a985b6cf)
|
||||||
|
* refactor disabling application experience and document better | [45a3669](https://github.com/undergroundwires/privacy.sexy/commit/45a3669443d82855a52f60524d341c15f380f9e7)
|
||||||
|
* escape printed characters to prevent command injection #45 | [1260eea](https://github.com/undergroundwires/privacy.sexy/commit/1260eea690e4fa5420e58c9de9f88cc29cb242db)
|
||||||
|
* move code area to right on bigger screens | [cf39e6d](https://github.com/undergroundwires/privacy.sexy/commit/cf39e6d2541ea547f41d9553c380c54c24c58038)
|
||||||
|
* more scripts to disable speech recognition and Cortana | [ee43fd9](https://github.com/undergroundwires/privacy.sexy/commit/ee43fd92a019ebd26c13890f9146c5b5bb56afaf)
|
||||||
|
* add more macos scripts for privacy cleanup | [b0a7d0b](https://github.com/undergroundwires/privacy.sexy/commit/b0a7d0b53b3d8ac144a0241d70c037f460b0c0cc)
|
||||||
|
* add better error messages to setting vscode settings | [65226f3](https://github.com/undergroundwires/privacy.sexy/commit/65226f3984480d0bc7932fd8d76a328f08308850)
|
||||||
|
* remove windows scripts for removing non-bloating system apps #55 | [15004ff](https://github.com/undergroundwires/privacy.sexy/commit/15004ff1f1fb85a1d92e11ef695bcb2f37110610)
|
||||||
|
* remove "preview" disclaimer from macOS | [970221b](https://github.com/undergroundwires/privacy.sexy/commit/970221b996e25fe5b029cbaa78607c9bbc8c3c0e)
|
||||||
|
* update screenshot | [bd41af4](https://github.com/undergroundwires/privacy.sexy/commit/bd41af466fd135f7dc2f171633e4f60d8547c373)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.9.2...0.10.0)
|
||||||
|
|
||||||
|
## 0.9.2 (2021-02-13)
|
||||||
|
|
||||||
|
* do not compile with unused locals vuejs/vetur#1063 | [73e0520](https://github.com/undergroundwires/privacy.sexy/commit/73e0520de70cdbaf0ecdc6e9be5e85f003fcfb79)
|
||||||
|
* 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)
|
## 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, 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)
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
- Proposing new features
|
- Proposing new features
|
||||||
- Becoming a maintainer
|
- Becoming a maintainer
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull request process
|
||||||
|
|
||||||
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) with [GitOps](./img/architecture/gitops.png) is used
|
||||||
- Your pull requests are actively welcomed.
|
- Your pull requests are actively welcomed.
|
||||||
- The steps:
|
- The steps:
|
||||||
1. Fork the repo and create your branch from master.
|
1. Fork the repo and create your branch from master.
|
||||||
@@ -25,4 +25,10 @@
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
By contributing, you agree that your contributions will be licensed under its [GNU General Public License v3.0](./LICENSE).
|
||||||
|
|
||||||
|
## Read more
|
||||||
|
|
||||||
|
- See [tests](./docs/tests.md) for testing
|
||||||
|
- See [extend script](./README.md#extend-scripts) for quick steps to extend scripts
|
||||||
|
- See [architecture overview](./README.md#architecture-overview) to deep dive into privacy.sexy codebase
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -14,11 +14,13 @@
|
|||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
- Online version at [https://privacy.sexy](https://privacy.sexy)
|
||||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-Setup-0.9.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.9.1/privacy.sexy-0.9.1.dmg)
|
- 💡 No need to run any compiled software on your computer.
|
||||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-Setup-0.10.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.10.3/privacy.sexy-0.10.3.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
|
## Why
|
||||||
|
|
||||||
@@ -28,21 +30,25 @@
|
|||||||
- Have full visibility into what the tweaks do as you enable them
|
- Have full visibility into what the tweaks do as you enable them
|
||||||
- Ability to revert (undo) applied scripts
|
- Ability to revert (undo) applied scripts
|
||||||
- Everything is transparent: both application and its infrastructure are open-source and automated
|
- Everything is transparent: both application and its infrastructure are open-source and automated
|
||||||
- Easily extendable
|
- Easily extendable with [own powerful templating language](./docs/templating.md)
|
||||||
|
- Each script is independently executable without cross-dependencies
|
||||||
|
|
||||||
## Extend scripts
|
## Extend scripts
|
||||||
|
|
||||||
1. Fork the repository
|
- You can either [create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose)
|
||||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
- Or send a PR:
|
||||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
1. Fork the repository
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
||||||
3. Send a pull request 👌
|
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
||||||
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
3. Send a pull request 👌
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- Project setup: `npm install`
|
- Project setup: `npm install`
|
||||||
- Testing
|
- Testing
|
||||||
- Run unit tests: `npm run test:unit`
|
- Run unit tests: `npm run test:unit`
|
||||||
|
- Run integration tests: `npm run test:integration`
|
||||||
- Lint: `npm run lint`
|
- Lint: `npm run lint`
|
||||||
- **Desktop app**
|
- **Desktop app**
|
||||||
- Development: `npm run electron:serve`
|
- Development: `npm run electron:serve`
|
||||||
@@ -51,8 +57,8 @@
|
|||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.9.1 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.10.3 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.9.1 undergroundwires/privacy.sexy:0.9.1`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.10.3 undergroundwires/privacy.sexy:0.10.3`
|
||||||
|
|
||||||
## Architecture overview
|
## Architecture overview
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
|
|
||||||
- It's mainly responsible for
|
- It's mainly responsible for
|
||||||
- creating and event based [application state](#application-state)
|
- creating and event based [application state](#application-state)
|
||||||
- parsing and compiling [application data](#application-data)
|
- [parsing](#parsing) and [compiling](#compiling) [application data](#application-data)
|
||||||
|
- Consumed by [presentation layer](./presentation.md)
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- [`/src/` **`application/`**](./../src/application/): Contains all application related code.
|
||||||
|
- [**`collections/`**](./../src/application/collections/): Holds [collection files](./collection-files.md)
|
||||||
|
- [**`Common/`**](./../src/application/Common/): Contains common functionality that is shared in application layer.
|
||||||
|
- `..`: other classes are categorized using folders-by-feature structure
|
||||||
|
|
||||||
## Application state
|
## Application state
|
||||||
|
|
||||||
@@ -14,9 +22,23 @@
|
|||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Compiled to `Application` domain object.
|
- Compiled to [`Application`](./../src/domain/Application.ts) domain object.
|
||||||
- The scripts are defined and controlled in different data files per OS
|
- The scripts are defined and controlled in different data files per OS
|
||||||
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
- Enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) and easier contributions
|
||||||
- Application data is defined in collection files and
|
- Application data is defined in collection files and
|
||||||
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
- 📖 See [Application data | Presentation layer](./presentation.md#application-data) to read how the application data is read by the presentation layer.
|
||||||
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
- 📖 See [collection files documentation](./collection-files.md) to read more about how the data files are structured/defined and see [collection yaml files](./../src/application/collections/) to directly check the code.
|
||||||
|
|
||||||
|
## Parsing
|
||||||
|
|
||||||
|
- Application data is parsed to domain object [`Application.ts`](./../src/domain/Application.ts)
|
||||||
|
- Steps
|
||||||
|
1. (Compile time) Load application data from [collection yaml files](./../src/application/collections/) using webpack loader
|
||||||
|
2. (Runtime) Parse and compile application and make it available to presentation layer by [`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)
|
||||||
|
|
||||||
|
### Compiling
|
||||||
|
|
||||||
|
- Parsing the application files includes compiling scripts using [collection file defined functions](./collection-files.md#function)
|
||||||
|
- To extend the syntax:
|
||||||
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts)
|
||||||
|
|||||||
@@ -45,9 +45,11 @@
|
|||||||
### `Script`
|
### `Script`
|
||||||
|
|
||||||
- Script represents a single tweak.
|
- Script represents a single tweak.
|
||||||
- A script must include either:
|
- A script can be of two different types (just like [functions](#function)):
|
||||||
- A `code` and `revertCode`
|
1. Inline script; a script with an inline code
|
||||||
- Or `call` to call YAML-defined functions
|
- Must define `code` property and optionally `revertCode` but not `call`
|
||||||
|
2. Caller script; a script that calls other functions
|
||||||
|
- Must define `call` property but not `code` or `revertCode`
|
||||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
#### `Script` syntax
|
#### `Script` syntax
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
### `FunctionCall`
|
### `FunctionCall`
|
||||||
|
|
||||||
- Describes a single call to a function by optionally providing values to its parameters.
|
- Describes a single call to a function by optionally providing values to its parameters.
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
- 👀 See [parameter substitution](./templating.md#parameter-substitution) for an example usage
|
||||||
|
|
||||||
#### `FunctionCall` syntax
|
#### `FunctionCall` syntax
|
||||||
|
|
||||||
@@ -98,32 +100,18 @@
|
|||||||
appName: Microsoft.WindowsFeedbackHub
|
appName: Microsoft.WindowsFeedbackHub
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value
|
||||||
|
|
||||||
### `Function`
|
### `Function`
|
||||||
|
|
||||||
- Functions allow re-usable code throughout the defined scripts.
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
- Functions are templates compiled by privacy.sexy and uses special expression expressions.
|
||||||
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
- A function can be of two different types (just like [scripts](#script)):
|
||||||
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
1. Inline function: a function with an inline code.
|
||||||
|
- Must define `code` property and optionally `revertCode` but not `call`.
|
||||||
#### Parameter substitution
|
2. Caller function: a function that calls other functions.
|
||||||
|
- Must define `call` property but not `code` or `revertCode`.
|
||||||
A simple function example
|
- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution).
|
||||||
|
|
||||||
```yaml
|
|
||||||
function: EchoArgument
|
|
||||||
parameters: [ 'argument' ]
|
|
||||||
code: Hello {{ $argument }} !
|
|
||||||
```
|
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](#script) as following:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
script: Echo script
|
|
||||||
call:
|
|
||||||
function: EchoArgument
|
|
||||||
parameters:
|
|
||||||
argument: World
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `Function` syntax
|
#### `Function` syntax
|
||||||
|
|
||||||
@@ -132,18 +120,42 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
|||||||
- Convention is to use camelCase, and be verbs.
|
- Convention is to use camelCase, and be verbs.
|
||||||
- E.g. `uninstallStoreApp`
|
- E.g. `uninstallStoreApp`
|
||||||
- ❗ Function names must be unique
|
- ❗ Function names must be unique
|
||||||
- `parameters`: `[` *`string`* `, ... ]`
|
- `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]`
|
||||||
- Name of the parameters that the function has.
|
- List of parameters that function code refers to.
|
||||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
- ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions)
|
||||||
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
`code`: *`string`* (**required** if `call` is undefined)
|
||||||
- ❗ Parameter names must be unique
|
|
||||||
`code`: *`string`* (**required**)
|
|
||||||
- Batch file commands that will be executed
|
- Batch file commands that will be executed
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in its value
|
||||||
- 💡 If defined, best practice to also define `revertCode`
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- ❗ If not defined `call` must be defined
|
||||||
- `revertCode`: *`string`*
|
- `revertCode`: *`string`*
|
||||||
- Code that'll undo the change done by `code` property.
|
- 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`
|
- 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`
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
- 💡 [Expressions (templating)](./templating.md#expressions) can be used in code
|
||||||
|
- `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 (templating)](./templating.md#expressions)
|
||||||
|
- ❗ If not defined `code` must be defined
|
||||||
|
|
||||||
|
### `FunctionParameter`
|
||||||
|
|
||||||
|
- Defines a parameter that function requires optionally or mandatory.
|
||||||
|
- Its arguments are provided by a [Script](#script) through a [FunctionCall](#FunctionCall).
|
||||||
|
|
||||||
|
#### `FunctionParameter` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the parameters that the function has.
|
||||||
|
- Parameter names must be defined to be used in [expressions (templating)](./templating.md#expressions).
|
||||||
|
- ❗ Parameter names must be unique and include alphanumeric characters only.
|
||||||
|
- `optional`: *`boolean`* (default: `false`)
|
||||||
|
- Specifies whether the caller [Script](#script) must provide any value for the parameter.
|
||||||
|
- If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable;
|
||||||
|
- Otherwise it throws.
|
||||||
|
- 💡 Set it to `true` if a parameter is used conditionally;
|
||||||
|
- Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
|
||||||
|
- 💡 Can be used in conjunction with [`with` expression](./templating.md#with).
|
||||||
|
|
||||||
### `ScriptingDefinition`
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
@@ -155,7 +167,7 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
|||||||
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||||
- `startCode:` *`string`* (**required**)
|
- `startCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted on top of user created script.
|
- Code that'll be inserted on top of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
- `endCode:` *`string`* (**required**)
|
- `endCode:` *`string`* (**required**)
|
||||||
- Code that'll be inserted at the end of user created script.
|
- Code that'll be inserted at the end of user created script.
|
||||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](./templating.md#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
- Event driven as in components simply listens to events from the state and act accordingly.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains all presentation related code including Vue and Electron configurations
|
||||||
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
|
||||||
|
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
|
||||||
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that are shared across other components.
|
||||||
|
- [**`styles/`**](./../src/presentation/styles/): Contains shared styles used throughout different components.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Application entry point that mounts and starts Vue application.
|
||||||
|
- [`electron/`](./../src/presentation/electron/): Electron configuration for the desktop application.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Main process of Electron, started as first thing when app starts.
|
||||||
|
- [**`/public/`**](./../public/): Contains static assets that will simply be copied and not go through webpack.
|
||||||
|
- [**`/vue.config.js`**](./../vue.config.js): Global Vue CLI configurations loaded by `@vue/cli-service`
|
||||||
|
- [**`/postcss.config.js`**](./../postcss.config.js): PostCSS configurations that are used by Vue CLI internally
|
||||||
|
- [**`/babel.config.js`**](./../babel.config.js): Babel configurations for polyfills used by `@vue/cli-plugin-babel`
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
- Components and should use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain.
|
||||||
@@ -16,9 +31,22 @@
|
|||||||
|
|
||||||
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
- Stateful components mutate or/and react to state changes in [ApplicationContext](./../src/application/Context/ApplicationContext.ts).
|
||||||
- Stateless components that does not handle state extends `Vue`
|
- Stateless components that does not handle state extends `Vue`
|
||||||
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
- Stateful components that depends on the collection state such as user selection, search queries and more extends [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
||||||
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/StatefulVue.ts)
|
- The single source of truth is a singleton of the state created and made available to presentation layer by [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts)
|
||||||
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
- `StatefulVue` includes abstract `handleCollectionState` that is fired once the component is loaded and also each time [collection](./collection-files.md) is changed.
|
||||||
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
- Do not forget to subscribe from events when component is destroyed or if needed [collection](./collection-files.md) is changed.
|
||||||
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/StatefulVue.ts) makes lifecycling easier
|
- 💡 `events` in base class [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) makes lifecycling easier
|
||||||
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
- 📖 See [Application state | Application layer](./presentation.md#application-state) where the state is implemented using using state pattern.
|
||||||
|
|
||||||
|
## Modals
|
||||||
|
|
||||||
|
- [Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that can be used to show modal windows
|
||||||
|
- Simply wrap the content inside of its slot and call `.show()` method on its reference.
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Dialog ref="testDialog">
|
||||||
|
<div>Hello world</div>
|
||||||
|
</Dialog>
|
||||||
|
<div @click="$refs.testDialog.show()">Show dialog</div>
|
||||||
|
```
|
||||||
|
|||||||
88
docs/templating.md
Normal file
88
docs/templating.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Templating
|
||||||
|
|
||||||
|
## Benefits of templating
|
||||||
|
|
||||||
|
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
||||||
|
- Creating self-contained scripts without depending on each other that can be easily shared.
|
||||||
|
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
||||||
|
|
||||||
|
## Expressions
|
||||||
|
|
||||||
|
- Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`).
|
||||||
|
- Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template).
|
||||||
|
- Expressions are used in and enabled by functions where they can be used.
|
||||||
|
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
||||||
|
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
||||||
|
|
||||||
|
### Parameter substitution
|
||||||
|
|
||||||
|
A simple function example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
A function can call other functions such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
function: CallerFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'value'
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: {{ $value }}
|
||||||
|
-
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
### with
|
||||||
|
|
||||||
|
- Skips the block if the variable is absent or empty.
|
||||||
|
- Binds its context (`.`) value of provided argument for the parameter if provided one.
|
||||||
|
- A block is defined as `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||||
|
- The parameters used for `with` condition should be declared as optional, otherwise `with` block becomes redundant.
|
||||||
|
- Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: FunctionThatOutputsConditionally
|
||||||
|
parameters:
|
||||||
|
- name: 'argument'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $argument }}
|
||||||
|
Value is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pipes
|
||||||
|
|
||||||
|
- Pipes are set of functions available for handling text in privacy.sexy.
|
||||||
|
- Allows stacking actions one after another also known as "chaining".
|
||||||
|
- Just like [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), the concept is simple: each pipeline's output becomes the input of the following pipe.
|
||||||
|
- Pipes are provided and defined by the compiler and consumed by collection files.
|
||||||
|
- Pipes can be combined with [parameter substitution](#parameter-substitution) and [with](#with).
|
||||||
|
- ❗ Pipe names must be camelCase without any space or special characters.
|
||||||
|
- **Existing pipes**
|
||||||
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
|
- `escapeDoubleQuotes`: Escapes `"` characters to be used inside double quotes (`"`)
|
||||||
|
- **Example usages**
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
||||||
|
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
||||||
43
docs/tests.md
Normal file
43
docs/tests.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
- There are two different types of tests executed:
|
||||||
|
1. [Unit tests](#unit-tests)
|
||||||
|
2. [Integration tests](#integration-tests)
|
||||||
|
- 💡 You can use path/module alias `@/tests` in import statements.
|
||||||
|
|
||||||
|
## Unit tests
|
||||||
|
|
||||||
|
- Tests each component in isolation
|
||||||
|
- Defined in [`./tests/unit`](./../tests/unit)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
## Integration tests
|
||||||
|
|
||||||
|
- Tests functionality of a component in combination with others (not isolated)
|
||||||
|
- Ensure dependencies to third parties work as expected
|
||||||
|
- Defined in [`./tests/integration`](./../tests/integration)
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 483 KiB After Width: | Height: | Size: 579 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 98 KiB |
23382
package-lock.json
generated
23382
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.9.1",
|
"version": "0.10.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
|
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
||||||
"electron:build": "vue-cli-service electron:build",
|
"electron:build": "vue-cli-service electron:build",
|
||||||
"electron:serve": "vue-cli-service electron:serve",
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
@@ -21,47 +22,51 @@
|
|||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
"@fortawesome/free-brands-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.2",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
|
"@juggle/resize-observer": "^3.3.1",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.12.1",
|
||||||
|
"cross-fetch": "^3.1.4",
|
||||||
|
"electron-progressbar": "^2.0.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.5",
|
"inversify": "^5.1.1",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.1.3",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.6",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
"vue-property-decorator": "^9.1.2"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.44",
|
"@types/ace": "0.0.45",
|
||||||
"@types/chai": "^4.2.14",
|
"@types/chai": "^4.2.18",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.2",
|
||||||
"@types/mocha": "^8.2.0",
|
"@types/mocha": "^8.2.2",
|
||||||
"@vue/cli-plugin-babel": "^4.5.10",
|
"@vue/cli-plugin-babel": "^4.5.13",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.9",
|
"@vue/cli-plugin-typescript": "^4.5.13",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
"@vue/cli-plugin-unit-mocha": "^4.5.13",
|
||||||
"@vue/cli-service": "^4.5.9",
|
"@vue/cli-service": "^4.5.13",
|
||||||
"@vue/test-utils": "1.1.2",
|
"@vue/test-utils": "1.2.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.3.4",
|
||||||
"electron": "^11.1.0",
|
"electron": "^12.0.7",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-log": "^4.3.1",
|
"electron-log": "^4.3.5",
|
||||||
"electron-updater": "^4.3.5",
|
"electron-updater": "^4.3.8",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.26.0",
|
"markdownlint-cli": "^0.27.1",
|
||||||
"remark-cli": "^9.0.0",
|
"remark-cli": "^9.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^4.0.0",
|
"remark-preset-lint-consistent": "^4.0.0",
|
||||||
"remark-validate-links": "^10.0.2",
|
"remark-validate-links": "^10.0.4",
|
||||||
"sass": "^1.30.0",
|
"sass": "^1.32.12",
|
||||||
"sass-loader": "^10.1.0",
|
"sass-loader": "^10.0.1",
|
||||||
"typescript": "^4.1.3",
|
"tslib": "^2.2.0",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
|
"typescript": "^4.2.4",
|
||||||
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.6",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
79
src/App.vue
79
src/App.vue
@@ -1,79 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<div class="wrapper">
|
|
||||||
<TheHeader class="row" />
|
|
||||||
<TheSearchBar class="row" />
|
|
||||||
<TheScripts class="row"/>
|
|
||||||
<TheCodeArea class="row" theme="xcode" />
|
|
||||||
<TheCodeButtons class="row code-buttons" />
|
|
||||||
<TheFooter />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
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 TheSearchBar from '@/presentation/TheSearchBar.vue';
|
|
||||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
components: {
|
|
||||||
TheHeader,
|
|
||||||
TheCodeArea,
|
|
||||||
TheCodeButtons,
|
|
||||||
TheScripts,
|
|
||||||
TheSearchBar,
|
|
||||||
TheFooter,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
export default class App extends Vue {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import "@/presentation/styles/colors.scss";
|
|
||||||
@import "@/presentation/styles/fonts.scss";
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: $light-gray;
|
|
||||||
font-family: $main-font;
|
|
||||||
color: $slate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#app {
|
|
||||||
margin-right: auto;
|
|
||||||
margin-left: auto;
|
|
||||||
max-width: 1500px;
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
margin: 0% 2% 0% 2%;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
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/Common/Array.ts
Normal file
21
src/application/Common/Array.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
|
const sortedArray1 = sort(array1);
|
||||||
|
const sortedArray2 = sort(array2);
|
||||||
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
|
function sort(array: readonly T[]) {
|
||||||
|
return array.slice().sort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares to Array<T> objects for equality in same order
|
||||||
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
|
if (!array1) { throw new Error('undefined first array'); }
|
||||||
|
if (!array2) { throw new Error('undefined second array'); }
|
||||||
|
if (array1.length !== array2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return array1.every((val, index) => val === array2[index]);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
type EnumType = number | string;
|
export type EnumType = number | string;
|
||||||
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface IEnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
@@ -41,3 +41,14 @@ export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
return getEnumNames(enumVariable)
|
return getEnumNames(enumVariable)
|
||||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: TEnumValue,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>) {
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error('undefined enum value');
|
||||||
|
}
|
||||||
|
if (!(value in enumVariable)) {
|
||||||
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface IScriptingLanguageFactory<T> {
|
||||||
|
create(language: ScriptingLanguage): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
|
||||||
|
type Getter<T> = () => T;
|
||||||
|
|
||||||
|
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
|
||||||
|
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
|
||||||
|
|
||||||
|
public create(language: ScriptingLanguage): T {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!this.getters.has(language)) {
|
||||||
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
const getter = this.getters.get(language);
|
||||||
|
const instance = getter();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
|
assertInRange(language, ScriptingLanguage);
|
||||||
|
if (!getter) {
|
||||||
|
throw new Error('undefined getter');
|
||||||
|
}
|
||||||
|
if (this.getters.has(language)) {
|
||||||
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
|
}
|
||||||
|
this.getters.set(language, getter);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { IApplication } from '@/domain/IApplication';
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
|
||||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem) {
|
initialContext: OperatingSystem) {
|
||||||
validateApp(app);
|
validateApp(app);
|
||||||
validateOs(initialContext);
|
assertInRange(initialContext, OperatingSystem);
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -50,18 +51,6 @@ function validateApp(app: IApplication) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOs(os: OperatingSystem) {
|
|
||||||
if (os === undefined) {
|
|
||||||
throw new Error('undefined os');
|
|
||||||
}
|
|
||||||
if (os === OperatingSystem.Unknown) {
|
|
||||||
throw new Error('unknown os');
|
|
||||||
}
|
|
||||||
if (!(os in OperatingSystem)) {
|
|
||||||
throw new Error(`os "${os}" is out of range`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
|
||||||
import { BatchBuilder } from './Languages/BatchBuilder';
|
import { BatchBuilder } from './Languages/BatchBuilder';
|
||||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
|
||||||
export class CodeBuilderFactory implements ICodeBuilderFactory {
|
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
|
||||||
public create(language: ScriptingLanguage): ICodeBuilder {
|
constructor() {
|
||||||
switch (language) {
|
super();
|
||||||
case ScriptingLanguage.shellscript: return new ShellBuilder();
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
|
||||||
case ScriptingLanguage.batchfile: return new BatchBuilder();
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
|
||||||
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { ICodeBuilder } from './ICodeBuilder';
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
|
||||||
export interface ICodeBuilderFactory {
|
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
|
||||||
create(language: ScriptingLanguage): ICodeBuilder;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export class BatchBuilder extends CodeBuilder {
|
|||||||
return '::';
|
return '::';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
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 '#';
|
return '#';
|
||||||
}
|
}
|
||||||
protected writeStandardOut(text: string): string {
|
protected writeStandardOut(text: string): string {
|
||||||
return `echo '${text}'`;
|
return `echo '${escapeForEcho(text)}'`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeForEcho(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/'/g, '\'\\\'\'');
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
|||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
|
||||||
areAllSelected(category: ICategory): boolean;
|
areAllSelected(category: ICategory): boolean;
|
||||||
isAnySelected(category: ICategory): boolean;
|
isAnySelected(category: ICategory): boolean;
|
||||||
removeAllInCategory(categoryId: number): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
|
|||||||
@@ -101,10 +101,6 @@ export class UserSelection implements IUserSelection {
|
|||||||
return this.scripts.getItems();
|
return this.scripts.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get totalSelected(): number {
|
|
||||||
return this.scripts.getItems().length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.collection.getAllScripts()) {
|
for (const script of this.collection.getAllScripts()) {
|
||||||
if (!this.scripts.exists(script.id)) {
|
if (!this.scripts.exists(script.id)) {
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
|
|||||||
|
|
||||||
export class BrowserOsDetector implements IBrowserOsDetector {
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
private readonly detectors = BrowserDetectors;
|
private readonly detectors = BrowserDetectors;
|
||||||
public detect(userAgent: string): OperatingSystem {
|
public detect(userAgent: string): OperatingSystem | undefined {
|
||||||
if (!userAgent) {
|
if (!userAgent) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
for (const detector of this.detectors) {
|
for (const detector of this.detectors) {
|
||||||
const os = detector.detect(userAgent);
|
const os = detector.detect(userAgent);
|
||||||
if (os !== OperatingSystem.Unknown) {
|
if (os !== undefined) {
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export class DetectorBuilder {
|
|||||||
throw new Error('User agent is null or undefined');
|
throw new Error('User agent is null or undefined');
|
||||||
}
|
}
|
||||||
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.os;
|
return this.os;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem;
|
detect(userAgent: string): OperatingSystem | undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
|
|||||||
return variables.process.platform;
|
return variables.process.platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDesktopOsType(processPlatform: string): OperatingSystem {
|
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
|
||||||
// https://nodejs.org/api/process.html#process_process_platform
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
if (processPlatform === 'darwin') {
|
if (processPlatform === 'darwin') {
|
||||||
return OperatingSystem.macOS;
|
return OperatingSystem.macOS;
|
||||||
@@ -53,7 +53,7 @@ function getDesktopOsType(processPlatform: string): OperatingSystem {
|
|||||||
} else if (processPlatform === 'linux') {
|
} else if (processPlatform === 'linux') {
|
||||||
return OperatingSystem.Linux;
|
return OperatingSystem.Linux;
|
||||||
}
|
}
|
||||||
return OperatingSystem.Unknown;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDesktop(variables: IEnvironmentVariables): boolean {
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ import { Category } from '@/domain/Category';
|
|||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export function parseCategoryCollection(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||||
validate(content);
|
validate(content);
|
||||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
const scripting = new ScriptingDefinitionParser()
|
||||||
|
.parse(content.scripting, info);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
const categories = new Array<Category>();
|
const categories = new Array<Category>();
|
||||||
for (const action of content.actions) {
|
for (const action of content.actions) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { FunctionData } from 'js-yaml-loader!*';
|
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
import { IExpression } from './IExpression';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
|
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
|
|
||||||
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
|
export class Expression implements IExpression {
|
||||||
|
constructor(
|
||||||
|
public readonly position: ExpressionPosition,
|
||||||
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
|
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
||||||
|
if (!position) {
|
||||||
|
throw new Error('undefined position');
|
||||||
|
}
|
||||||
|
if (!evaluator) {
|
||||||
|
throw new Error('undefined evaluator');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('undefined context');
|
||||||
|
}
|
||||||
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
|
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
return this.evaluator(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
) {
|
||||||
|
const requiredParameterNames = parameters
|
||||||
|
.all
|
||||||
|
.filter((parameter) => !parameter.isOptional)
|
||||||
|
.map((parameter) => parameter.name);
|
||||||
|
const missingParameterNames = requiredParameterNames
|
||||||
|
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||||
|
if (missingParameterNames.length) {
|
||||||
|
throw new Error(
|
||||||
|
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterUnusedArguments(
|
||||||
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||||
|
for (const parameter of parameters.all) {
|
||||||
|
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
||||||
|
continue; // Optional parameter is not necessarily provided
|
||||||
|
}
|
||||||
|
const arg = allFunctionArgs.getArgument(parameter.name);
|
||||||
|
specificCallArgs.addArgument(arg);
|
||||||
|
}
|
||||||
|
return specificCallArgs;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
||||||
|
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||||
|
|
||||||
|
export interface IExpressionEvaluationContext {
|
||||||
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
|
readonly pipelineCompiler: IPipelineCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||||
|
constructor(
|
||||||
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args, send empty collection instead');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export class ExpressionPosition {
|
||||||
|
constructor(
|
||||||
|
public readonly start: number,
|
||||||
|
public readonly end: number) {
|
||||||
|
if (start === end) {
|
||||||
|
throw new Error(`no length (start = end = ${start})`);
|
||||||
|
}
|
||||||
|
if (start > end) {
|
||||||
|
throw Error(`start (${start}) after end (${end})`);
|
||||||
|
}
|
||||||
|
if (start < 0) {
|
||||||
|
throw Error(`negative start position: ${start}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
|
|
||||||
|
export interface IExpression {
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
evaluate(context: IExpressionEvaluationContext): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
|
import { IExpression } from './Expression/IExpression';
|
||||||
|
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||||
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
|
||||||
|
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
|
|
||||||
|
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||||
|
public constructor(
|
||||||
|
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||||
|
public compileExpressions(
|
||||||
|
code: string,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection): string {
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args, send empty collection instead');
|
||||||
|
}
|
||||||
|
const expressions = this.extractor.findExpressions(code);
|
||||||
|
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||||
|
const context = new ExpressionEvaluationContext(args);
|
||||||
|
const compiledCode = compileExpressions(expressions, code, context);
|
||||||
|
return compiledCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileExpressions(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
code: string,
|
||||||
|
context: IExpressionEvaluationContext) {
|
||||||
|
let compiledCode = '';
|
||||||
|
const sortedExpressions = expressions
|
||||||
|
.slice() // copy the array to not mutate the parameter
|
||||||
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
|
let index = 0;
|
||||||
|
while (index !== code.length) {
|
||||||
|
const nextExpression = sortedExpressions.pop();
|
||||||
|
if (nextExpression) {
|
||||||
|
compiledCode += code.substring(index, nextExpression.position.start);
|
||||||
|
const expressionCode = nextExpression.evaluate(context);
|
||||||
|
compiledCode += expressionCode;
|
||||||
|
index = nextExpression.position.end;
|
||||||
|
} else {
|
||||||
|
compiledCode += code.substring(index, code.length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return compiledCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRequiredParameterNames(
|
||||||
|
expressions: readonly IExpression[]): string[] {
|
||||||
|
const usedParameterNames = expressions
|
||||||
|
.map((e) => e.parameters.all
|
||||||
|
.filter((p) => !p.isOptional)
|
||||||
|
.map((p) => p.name))
|
||||||
|
.filter((p) => p)
|
||||||
|
.flat();
|
||||||
|
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
||||||
|
return uniqueParameterNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
|
expressions: readonly IExpression[],
|
||||||
|
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
||||||
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
|
if (!usedParameterNames?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notProvidedParameters = usedParameterNames
|
||||||
|
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||||
|
if (notProvidedParameters.length) {
|
||||||
|
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('", "')}"`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
|
export interface IExpressionsCompiler {
|
||||||
|
compileExpressions(
|
||||||
|
code: string,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
import { IExpressionParser } from './IExpressionParser';
|
||||||
|
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||||
|
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||||
|
|
||||||
|
const Parsers = [
|
||||||
|
new ParameterSubstitutionParser(),
|
||||||
|
new WithParser(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
|
if (leafs.some((leaf) => !leaf)) {
|
||||||
|
throw new Error('undefined leaf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
const expressions = new Array<IExpression>();
|
||||||
|
for (const parser of this.leafs) {
|
||||||
|
const newExpressions = parser.findExpressions(code);
|
||||||
|
if (newExpressions && newExpressions.length) {
|
||||||
|
expressions.push(...newExpressions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
|
||||||
|
export interface IExpressionParser {
|
||||||
|
findExpressions(code: string): IExpression[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export class ExpressionRegexBuilder {
|
||||||
|
private readonly parts = new Array<string>();
|
||||||
|
|
||||||
|
public expectCharacters(characters: string) {
|
||||||
|
return this.addRawRegex(
|
||||||
|
characters
|
||||||
|
.replaceAll('$', '\\$')
|
||||||
|
.replaceAll('.', '\\.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectOneOrMoreWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s+');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchPipeline() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.addRawRegex('(\\|\\s*.+?)?');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchUntilFirstWhitespace() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('([^|\\s]+)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchAnythingExceptSurroundingWhitespaces() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.addRawRegex('(.+?)')
|
||||||
|
.expectZeroOrMoreWhitespaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectExpressionStart() {
|
||||||
|
return this
|
||||||
|
.expectCharacters('{{')
|
||||||
|
.expectZeroOrMoreWhitespaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectExpressionEnd() {
|
||||||
|
return this
|
||||||
|
.expectZeroOrMoreWhitespaces()
|
||||||
|
.expectCharacters('}}');
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildRegExp(): RegExp {
|
||||||
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
private expectZeroOrMoreWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s*');
|
||||||
|
}
|
||||||
|
private addRawRegex(regex: string) {
|
||||||
|
this.parts.push(regex);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
|
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
||||||
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
|
||||||
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
return Array.from(this.findRegexExpressions(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||||
|
|
||||||
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
|
const matches = Array.from(code.matchAll(this.regex));
|
||||||
|
for (const match of matches) {
|
||||||
|
const startPos = match.index;
|
||||||
|
const endPos = startPos + match[0].length;
|
||||||
|
let position: ExpressionPosition;
|
||||||
|
try {
|
||||||
|
position = new ExpressionPosition(startPos, endPos);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
||||||
|
}
|
||||||
|
const primitiveExpression = this.buildExpression(match);
|
||||||
|
const parameters = getParameters(primitiveExpression);
|
||||||
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
|
yield expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPrimitiveExpression {
|
||||||
|
evaluator: ExpressionEvaluator;
|
||||||
|
parameters?: readonly IFunctionParameter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParameters(
|
||||||
|
expression: IPrimitiveExpression): FunctionParameterCollection {
|
||||||
|
const parameters = new FunctionParameterCollection();
|
||||||
|
for (const parameter of expression.parameters || []) {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IPipe {
|
||||||
|
readonly name: string;
|
||||||
|
apply(input: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IPipelineCompiler {
|
||||||
|
compile(value: string, pipeline: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
public apply(raw: string): string {
|
||||||
|
return raw?.replaceAll('"', '"^""');
|
||||||
|
/*
|
||||||
|
"^"" is the most robust and stable choice.
|
||||||
|
Other options:
|
||||||
|
""
|
||||||
|
Breaks, because it is fundamentally unsupported
|
||||||
|
""""
|
||||||
|
Does not work with consecutive double quotes.
|
||||||
|
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
||||||
|
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
||||||
|
\"
|
||||||
|
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||||
|
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
|
||||||
|
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
|
||||||
|
\""
|
||||||
|
Normalizes interior whitespace
|
||||||
|
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
||||||
|
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
||||||
|
A good explanation: https://stackoverflow.com/a/31413730
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { IPipe } from '../IPipe';
|
||||||
|
|
||||||
|
export class InlinePowerShell implements IPipe {
|
||||||
|
public readonly name: string = 'inlinePowerShell';
|
||||||
|
public apply(code: string): string {
|
||||||
|
if (!code || !hasLines(code)) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
code = replaceComments(code);
|
||||||
|
code = mergeLinesWithBacktick(code);
|
||||||
|
code = mergeHereStrings(code);
|
||||||
|
const lines = getLines(code)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
return lines
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLines(text: string) {
|
||||||
|
return text.includes('\n') || text.includes('\r');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||||
|
Otherwise single # comments out rest of the code
|
||||||
|
*/
|
||||||
|
function replaceComments(code: string) {
|
||||||
|
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (_$, match1 ) => {
|
||||||
|
const value = match1?.trim();
|
||||||
|
if (!value) {
|
||||||
|
return '<##>';
|
||||||
|
}
|
||||||
|
return `<# ${value} #>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLines(code: string) {
|
||||||
|
return (code.split(/\r\n|\r|\n/) || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
|
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||||
|
*/
|
||||||
|
function mergeHereStrings(code: string) {
|
||||||
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
|
const newString = getHereStringHandler(quotes);
|
||||||
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
|
const lines = getLines(escaped);
|
||||||
|
const inlined = lines.join(newString.separator);
|
||||||
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
|
return quoted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
interface IInlinedHereString {
|
||||||
|
readonly quotesAround: string;
|
||||||
|
readonly escapedQuotes: string;
|
||||||
|
readonly separator: string;
|
||||||
|
}
|
||||||
|
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
||||||
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
|
const expandableNewLine = '`r`n';
|
||||||
|
switch (quotes) {
|
||||||
|
case '\'':
|
||||||
|
return {
|
||||||
|
quotesAround: '\'',
|
||||||
|
escapedQuotes: '\'\'',
|
||||||
|
separator: `\'+"${expandableNewLine}"+\'`,
|
||||||
|
};
|
||||||
|
case '"':
|
||||||
|
return {
|
||||||
|
quotesAround: '"',
|
||||||
|
escapedQuotes: '`"',
|
||||||
|
separator: expandableNewLine,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new Error(`expected quotes: ${quotes}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Input ->
|
||||||
|
Get-Service * `
|
||||||
|
Sort-Object StartType `
|
||||||
|
Format-Table Name, ServiceType, Status -AutoSize
|
||||||
|
Output ->
|
||||||
|
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||||
|
*/
|
||||||
|
function mergeLinesWithBacktick(code: string) {
|
||||||
|
/*
|
||||||
|
The regex actually wraps any whitespace character after backtick and before newline
|
||||||
|
However, this is not always the case for PowerShell.
|
||||||
|
I see two behaviors:
|
||||||
|
1. If inside string, it's accepted (inside " or ')
|
||||||
|
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||||
|
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||||
|
we wrap it anyway
|
||||||
|
*/
|
||||||
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { IPipe } from './IPipe';
|
||||||
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
|
|
||||||
|
const RegisteredPipes = [
|
||||||
|
new EscapeDoubleQuotes(),
|
||||||
|
new InlinePowerShell(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface IPipeFactory {
|
||||||
|
get(pipeName: string): IPipe;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PipeFactory implements IPipeFactory {
|
||||||
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
|
if (pipes.some((pipe) => !pipe)) {
|
||||||
|
throw new Error('undefined pipe in list');
|
||||||
|
}
|
||||||
|
for (const pipe of pipes) {
|
||||||
|
this.registerPipe(pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public get(pipeName: string): IPipe {
|
||||||
|
validatePipeName(pipeName);
|
||||||
|
if (!this.pipes.has(pipeName)) {
|
||||||
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
|
}
|
||||||
|
return this.pipes.get(pipeName);
|
||||||
|
}
|
||||||
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
validatePipeName(pipe.name);
|
||||||
|
if (this.pipes.has(pipe.name)) {
|
||||||
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
|
}
|
||||||
|
this.pipes.set(pipe.name, pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePipeName(name: string) {
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('empty pipe name');
|
||||||
|
}
|
||||||
|
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||||
|
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { IPipeFactory, PipeFactory } from './PipeFactory';
|
||||||
|
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||||
|
|
||||||
|
export class PipelineCompiler implements IPipelineCompiler {
|
||||||
|
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||||
|
public compile(value: string, pipeline: string): string {
|
||||||
|
ensureValidArguments(value, pipeline);
|
||||||
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
|
for (const pipe of pipes) {
|
||||||
|
value = pipe.apply(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPipeNames(pipeline: string): string[] {
|
||||||
|
return pipeline
|
||||||
|
.trim()
|
||||||
|
.split('|')
|
||||||
|
.slice(1)
|
||||||
|
.map((p) => p.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidArguments(value: string, pipeline: string) {
|
||||||
|
if (!value) { throw new Error('undefined value'); }
|
||||||
|
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||||
|
if (!pipeline.trimStart().startsWith('|')) {
|
||||||
|
throw new Error('pipeline does not start with pipe');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||||
|
.matchPipeline() // Second match: Pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
|
const parameterName = match[1];
|
||||||
|
const pipeline = match[2];
|
||||||
|
return {
|
||||||
|
parameters: [ new FunctionParameter(parameterName, false) ],
|
||||||
|
evaluator: (context) => {
|
||||||
|
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
|
export class WithParser extends RegexParser {
|
||||||
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
|
// {{ with $parameterName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('with')
|
||||||
|
.expectOneOrMoreWhitespaces()
|
||||||
|
.expectCharacters('$')
|
||||||
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
// ...
|
||||||
|
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
|
// {{ end }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||||
|
const parameterName = match[1];
|
||||||
|
const scopeText = match[2];
|
||||||
|
return {
|
||||||
|
parameters: [ new FunctionParameter(parameterName, true) ],
|
||||||
|
evaluator: (context) => {
|
||||||
|
const argumentValue = context.args.hasArgument(parameterName) ?
|
||||||
|
context.args.getArgument(parameterName).argumentValue
|
||||||
|
: undefined;
|
||||||
|
if (!argumentValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||||
|
if (!pipeline) {
|
||||||
|
return argumentValue;
|
||||||
|
}
|
||||||
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ . | pipeName }}
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('.')
|
||||||
|
.matchPipeline() // First match: pipeline
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||||
|
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
||||||
|
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
||||||
|
return replacer(match1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
|
|
||||||
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
|
constructor(
|
||||||
|
public readonly parameterName: string,
|
||||||
|
public readonly argumentValue: string) {
|
||||||
|
ensureValidParameterName(parameterName);
|
||||||
|
if (!argumentValue) {
|
||||||
|
throw new Error(`undefined argument value for "${parameterName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
|
if (!argument) {
|
||||||
|
throw new Error('undefined argument');
|
||||||
|
}
|
||||||
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
|
}
|
||||||
|
this.arguments.set(argument.parameterName, argument);
|
||||||
|
}
|
||||||
|
public getAllParameterNames(): string[] {
|
||||||
|
return Array.from(this.arguments.keys());
|
||||||
|
}
|
||||||
|
public hasArgument(parameterName: string): boolean {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
|
}
|
||||||
|
return this.arguments.has(parameterName);
|
||||||
|
}
|
||||||
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
|
}
|
||||||
|
const arg = this.arguments.get(parameterName);
|
||||||
|
if (!arg) {
|
||||||
|
throw new Error(`parameter does not exist: ${parameterName}`);
|
||||||
|
}
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IFunctionCallArgument {
|
||||||
|
readonly parameterName: string;
|
||||||
|
readonly argumentValue: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
|
getAllParameterNames(): string[];
|
||||||
|
hasArgument(parameterName: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
addArgument(argument: IFunctionCallArgument): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||||
|
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
|
||||||
|
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
|
||||||
|
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
|
||||||
|
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
|
||||||
|
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
|
||||||
|
import { FunctionCall } from '../FunctionCall';
|
||||||
|
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
|
||||||
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
|
||||||
|
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||||
|
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public compileCall(
|
||||||
|
calls: IFunctionCall[],
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode {
|
||||||
|
if (!functions) { throw new Error('undefined functions'); }
|
||||||
|
if (!calls) { throw new Error('undefined calls'); }
|
||||||
|
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
|
||||||
|
const context: ICompilationContext = {
|
||||||
|
allFunctions: functions,
|
||||||
|
callSequence: calls,
|
||||||
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
|
};
|
||||||
|
const code = compileCallSequence(context);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICompilationContext {
|
||||||
|
allFunctions: ISharedFunctionCollection;
|
||||||
|
callSequence: readonly IFunctionCall[];
|
||||||
|
expressionsCompiler: IExpressionsCompiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICompiledFunctionCall {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||||
|
const compiledFunctions = new Array<ICompiledFunctionCall>();
|
||||||
|
for (const call of context.callSequence) {
|
||||||
|
const compiledCode = compileSingleCall(call, context);
|
||||||
|
compiledFunctions.push(...compiledCode);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: merge(compiledFunctions.map((f) => f.code)),
|
||||||
|
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
|
||||||
|
const func = context.allFunctions.getFunctionByName(call.functionName);
|
||||||
|
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
|
||||||
|
if (func.body.code) { // Function with inline code
|
||||||
|
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
|
||||||
|
return [ compiledCode ];
|
||||||
|
} else { // Function with inner calls
|
||||||
|
return func.body.calls
|
||||||
|
.map((innerCall) => {
|
||||||
|
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
|
||||||
|
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
|
||||||
|
return compileSingleCall(compiledCall, context);
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCode(
|
||||||
|
code: IFunctionCode,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
compiler: IExpressionsCompiler): ICompiledFunctionCall {
|
||||||
|
return {
|
||||||
|
code: compiler.compileExpressions(code.do, args),
|
||||||
|
revertCode: compiler.compileExpressions(code.revert, args),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileArgs(
|
||||||
|
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
compiler: IExpressionsCompiler,
|
||||||
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
|
const compiledArgs = new FunctionCallArgumentCollection();
|
||||||
|
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
||||||
|
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
|
||||||
|
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||||
|
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
||||||
|
compiledArgs.addArgument(newArgument);
|
||||||
|
}
|
||||||
|
return compiledArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(codeParts: readonly string[]): string {
|
||||||
|
return codeParts
|
||||||
|
.filter((part) => part?.length > 0)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureThatCallArgumentsExistInParameterDefinition(
|
||||||
|
func: ISharedFunction,
|
||||||
|
args: IReadOnlyFunctionCallArgumentCollection): void {
|
||||||
|
const callArgumentNames = args.getAllParameterNames();
|
||||||
|
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
|
||||||
|
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
|
||||||
|
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUnexpectedParameters(
|
||||||
|
callArgumentNames: string[],
|
||||||
|
functionParameterNames: string[]): string[] {
|
||||||
|
if (!callArgumentNames.length && !functionParameterNames.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return callArgumentNames
|
||||||
|
.filter((callParam) => !functionParameterNames.includes(callParam));
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfNotEmpty(
|
||||||
|
functionName: string,
|
||||||
|
unexpectedParameters: string[],
|
||||||
|
expectedParameters: string[]) {
|
||||||
|
if (!unexpectedParameters.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Function "${functionName}" has unexpected parameter(s) provided: ` +
|
||||||
|
`"${unexpectedParameters.join('", "')}"` +
|
||||||
|
'. Expected parameter(s): ' +
|
||||||
|
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ICompiledCode {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ICompiledCode } from './ICompiledCode';
|
||||||
|
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
|
import { IFunctionCall } from '../IFunctionCall';
|
||||||
|
|
||||||
|
export interface IFunctionCallCompiler {
|
||||||
|
compileCall(
|
||||||
|
calls: IFunctionCall[],
|
||||||
|
functions: ISharedFunctionCollection): ICompiledCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||||
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
|
|
||||||
|
export class FunctionCall implements IFunctionCall {
|
||||||
|
constructor(
|
||||||
|
public readonly functionName: string,
|
||||||
|
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
|
||||||
|
if (!functionName) {
|
||||||
|
throw new Error('empty function name in function call');
|
||||||
|
}
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('undefined args');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
|
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||||
|
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||||
|
import { FunctionCall } from './FunctionCall';
|
||||||
|
|
||||||
|
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
|
||||||
|
if (!calls) {
|
||||||
|
throw new Error('undefined call data');
|
||||||
|
}
|
||||||
|
const sequence = getCallSequence(calls);
|
||||||
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
||||||
|
if (typeof calls !== 'object') {
|
||||||
|
throw new Error('called function(s) must be an object');
|
||||||
|
}
|
||||||
|
if (calls instanceof Array) {
|
||||||
|
return calls as FunctionCallData[];
|
||||||
|
}
|
||||||
|
return [ calls as FunctionCallData ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
||||||
|
if (!call) {
|
||||||
|
throw new Error(`undefined function call`);
|
||||||
|
}
|
||||||
|
const args = new FunctionCallArgumentCollection();
|
||||||
|
for (const parameterName of Object.keys(call.parameters || {})) {
|
||||||
|
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
|
||||||
|
args.addArgument(arg);
|
||||||
|
}
|
||||||
|
return new FunctionCall(call.function, args);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
|
export interface IFunctionCall {
|
||||||
|
readonly functionName: string;
|
||||||
|
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||||
|
|
||||||
|
export interface ISharedFunction {
|
||||||
|
readonly name: string;
|
||||||
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
readonly body: ISharedFunctionBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISharedFunctionBody {
|
||||||
|
readonly type: FunctionBodyType;
|
||||||
|
readonly code: IFunctionCode;
|
||||||
|
readonly calls: readonly IFunctionCall[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FunctionBodyType {
|
||||||
|
Code,
|
||||||
|
Calls,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFunctionCode {
|
||||||
|
readonly do: string;
|
||||||
|
readonly revert?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
|
export interface ISharedFunctionCollection {
|
||||||
|
getFunctionByName(name: string): ISharedFunction;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { FunctionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
|
||||||
|
export interface ISharedFunctionsParser {
|
||||||
|
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { IFunctionParameter } from './IFunctionParameter';
|
||||||
|
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
|
||||||
|
|
||||||
|
export class FunctionParameter implements IFunctionParameter {
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly isOptional: boolean) {
|
||||||
|
ensureValidParameterName(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||||
|
import { IFunctionParameter } from './IFunctionParameter';
|
||||||
|
|
||||||
|
export class FunctionParameterCollection implements IFunctionParameterCollection {
|
||||||
|
private parameters = new Array<IFunctionParameter>();
|
||||||
|
|
||||||
|
public get all(): readonly IFunctionParameter[] {
|
||||||
|
return this.parameters;
|
||||||
|
}
|
||||||
|
public addParameter(parameter: IFunctionParameter) {
|
||||||
|
this.ensureValidParameter(parameter);
|
||||||
|
this.parameters.push(parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private includesName(name: string) {
|
||||||
|
return this.parameters.find((existingParameter) => existingParameter.name === name);
|
||||||
|
}
|
||||||
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
|
if (!parameter) {
|
||||||
|
throw new Error('undefined parameter');
|
||||||
|
}
|
||||||
|
if (this.includesName(parameter.name)) {
|
||||||
|
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IFunctionParameter {
|
||||||
|
readonly name: string;
|
||||||
|
readonly isOptional: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IFunctionParameter } from './IFunctionParameter';
|
||||||
|
|
||||||
|
export interface IReadOnlyFunctionParameterCollection {
|
||||||
|
readonly all: readonly IFunctionParameter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
|
||||||
|
addParameter(parameter: IFunctionParameter): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function ensureValidParameterName(parameterName: string) {
|
||||||
|
if (!parameterName) {
|
||||||
|
throw new Error('undefined parameter name');
|
||||||
|
}
|
||||||
|
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
|
||||||
|
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { IFunctionCall } from '../Function/Call/IFunctionCall';
|
||||||
|
import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction';
|
||||||
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export function createCallerFunction(
|
||||||
|
name: string,
|
||||||
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
callSequence: readonly IFunctionCall[]): ISharedFunction {
|
||||||
|
if (!callSequence) {
|
||||||
|
throw new Error(`undefined call sequence in function "${name}"`);
|
||||||
|
}
|
||||||
|
if (!callSequence.length) {
|
||||||
|
throw new Error(`empty call sequence in function "${name}"`);
|
||||||
|
}
|
||||||
|
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFunctionWithInlineCode(
|
||||||
|
name: string,
|
||||||
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
code: string,
|
||||||
|
revertCode?: string): ISharedFunction {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error(`undefined code in function "${name}"`);
|
||||||
|
}
|
||||||
|
const content: IFunctionCode = {
|
||||||
|
do: code,
|
||||||
|
revert: revertCode,
|
||||||
|
};
|
||||||
|
return new SharedFunction(name, parameters, content, FunctionBodyType.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SharedFunction implements ISharedFunction {
|
||||||
|
public readonly body: ISharedFunctionBody;
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly parameters: IReadOnlyFunctionParameterCollection,
|
||||||
|
content: IFunctionCode | readonly IFunctionCall[],
|
||||||
|
bodyType: FunctionBodyType,
|
||||||
|
) {
|
||||||
|
if (!name) { throw new Error('undefined function name'); }
|
||||||
|
if (!parameters) { throw new Error(`undefined parameters`); }
|
||||||
|
this.body = {
|
||||||
|
type: bodyType,
|
||||||
|
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
||||||
|
calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private has(functionName: string) {
|
||||||
|
return this.functionsByName.has(functionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { FunctionData, InstructionHolder } from 'js-yaml-loader!@/*';
|
||||||
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
|
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
|
import { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
||||||
|
import { FunctionParameter } from './Parameter/FunctionParameter';
|
||||||
|
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
|
||||||
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
import { ISharedFunction } from './ISharedFunction';
|
||||||
|
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||||
|
|
||||||
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
public parseFunctions(
|
||||||
|
functions: readonly FunctionData[]): ISharedFunctionCollection {
|
||||||
|
const collection = new SharedFunctionCollection();
|
||||||
|
if (!functions || !functions.length) {
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
ensureValidFunctions(functions);
|
||||||
|
for (const func of functions) {
|
||||||
|
const sharedFunction = parseFunction(func);
|
||||||
|
collection.addFunction(sharedFunction);
|
||||||
|
}
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFunction(data: FunctionData): ISharedFunction {
|
||||||
|
const name = data.name;
|
||||||
|
const parameters = parseParameters(data);
|
||||||
|
if (hasCode(data)) {
|
||||||
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
|
} else { // has call
|
||||||
|
const calls = parseFunctionCalls(data.call);
|
||||||
|
return createCallerFunction(name, parameters, calls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
|
const parameters = new FunctionParameterCollection();
|
||||||
|
if (!data.parameters) {
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
for (const parameterData of data.parameters) {
|
||||||
|
const isOptional = parameterData.optional || false;
|
||||||
|
try {
|
||||||
|
const parameter = new FunctionParameter(parameterData.name, isOptional);
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`"${data.name}": ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
ensureExpectedParametersType(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 ensureExpectedParametersType(functions: readonly FunctionData[]) {
|
||||||
|
const unexpectedFunctions = functions
|
||||||
|
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
|
||||||
|
if (unexpectedFunctions.length) {
|
||||||
|
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArrayOfObjects(value: any): boolean {
|
||||||
|
return Array.isArray(value)
|
||||||
|
&& value.every((item) => typeof item === 'object');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
export interface IILCode {
|
|
||||||
compile(): string;
|
|
||||||
getUniqueParameterNames(): string[];
|
|
||||||
substituteParameter(parameterName: string, parameterValue: string): IILCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateIlCode(rawText: string): IILCode {
|
|
||||||
const ilCode = generateIl(rawText);
|
|
||||||
return new ILCode(ilCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ILCode implements IILCode {
|
|
||||||
private readonly ilCode: string;
|
|
||||||
|
|
||||||
constructor(ilCode: string) {
|
|
||||||
this.ilCode = ilCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
|
|
||||||
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
|
|
||||||
return new ILCode(newCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUniqueParameterNames(): string[] {
|
|
||||||
return getUniqueParameterNames(this.ilCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public compile(): string {
|
|
||||||
ensureNoExpressionLeft(this.ilCode);
|
|
||||||
return this.ilCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
|
||||||
function generateIl(rawText: string): string {
|
|
||||||
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
|
||||||
return `\{\{exp|${match.trim()}\}\}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// finds all "{{exp|..}} left"
|
|
||||||
function ensureNoExpressionLeft(ilCode: string) {
|
|
||||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
|
||||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
|
||||||
const uniqueExpressions = getDistinctValues(allMatches);
|
|
||||||
if (uniqueExpressions.length > 0) {
|
|
||||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses all distinct usages of {{exp|$parameterName}}
|
|
||||||
function getUniqueParameterNames(ilCode: string) {
|
|
||||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
|
||||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
|
||||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
|
||||||
return uniqueParameterNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
|
||||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
|
||||||
const pattern = `{{exp|$${parameterName}}}`;
|
|
||||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDistinctValues(values: readonly string[]): string[] {
|
|
||||||
return values.filter((value, index, self) => {
|
|
||||||
return self.indexOf(value) === index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function printList(list: readonly string[]): string {
|
|
||||||
return `"${list.join('","')}"`;
|
|
||||||
}
|
|
||||||
@@ -1,184 +1,44 @@
|
|||||||
import { generateIlCode, IILCode } from './ILCode';
|
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
|
||||||
import { IScriptCompiler } from './IScriptCompiler';
|
|
||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { FunctionData, ScriptData } from 'js-yaml-loader!@/*';
|
||||||
interface ICompiledCode {
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
readonly code: string;
|
import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
||||||
readonly revertCode: string;
|
import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler';
|
||||||
}
|
import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
||||||
|
import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser';
|
||||||
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
|
private readonly functions: ISharedFunctionCollection;
|
||||||
constructor(
|
constructor(
|
||||||
private readonly functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[] | undefined,
|
||||||
private syntax: ILanguageSyntax) {
|
private readonly syntax: ILanguageSyntax,
|
||||||
ensureValidFunctions(functions);
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
|
private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance,
|
||||||
|
) {
|
||||||
if (!syntax) { throw new Error('undefined syntax'); }
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
|
this.functions = sharedFunctionsParser.parseFunctions(functions);
|
||||||
}
|
}
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
|
if (!script) { throw new Error('undefined script'); }
|
||||||
if (!script.call) {
|
if (!script.call) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
this.ensureCompilable(script.call);
|
if (!script) { throw new Error('undefined script'); }
|
||||||
const compiledCodes = new Array<ICompiledCode>();
|
try {
|
||||||
const calls = getCallSequence(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
calls.forEach((currentCall, currentCallIndex) => {
|
const compiledCode = this.callCompiler.compileCall(calls, this.functions);
|
||||||
ensureValidCall(currentCall, script.name);
|
return new ScriptCode(
|
||||||
const commonFunction = this.getFunctionByName(currentCall.function);
|
compiledCode.code,
|
||||||
ensureExpectedParameters(commonFunction, currentCall);
|
compiledCode.revertCode,
|
||||||
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
this.syntax);
|
||||||
if (currentCallIndex !== calls.length - 1) {
|
} catch (error) {
|
||||||
functionCode = appendLine(functionCode);
|
throw Error(`Script "${script.name}" ${error.message}`);
|
||||||
}
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(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) {
|
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
|
||||||
|
const BatchFileCommonCodeParts = [ '(', ')', 'else', '||' ];
|
||||||
|
const PowerShellCommonCodeParts = [ '{', '}' ];
|
||||||
|
|
||||||
export class BatchFileSyntax implements ILanguageSyntax {
|
export class BatchFileSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = [ 'REM', '::' ];
|
public readonly commentDelimiters = [ 'REM', '::' ];
|
||||||
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
public readonly commonCodeParts = [ ...BatchFileCommonCodeParts, ...PowerShellCommonCodeParts ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
|
||||||
|
|
||||||
export interface ISyntaxFactory {
|
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
|
||||||
create(language: ScriptingLanguage): ILanguageSyntax;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { ILanguageSyntax } from '@/domain/ScriptCode';
|
|||||||
|
|
||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = [ '#' ];
|
public readonly commentDelimiters = [ '#' ];
|
||||||
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
public readonly commonCodeParts = [ '(', ')', 'else', 'fi' ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
|
||||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
|
|
||||||
export class SyntaxFactory implements ISyntaxFactory {
|
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
|
||||||
public create(language: ScriptingLanguage): ILanguageSyntax {
|
constructor() {
|
||||||
switch (language) {
|
super();
|
||||||
case ScriptingLanguage.batchfile: return new BatchFileSyntax();
|
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
|
||||||
case ScriptingLanguage.shellscript: return new ShellScriptSyntax();
|
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
|
||||||
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
|
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
|
||||||
|
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||||
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
|
||||||
|
export class CodeSubstituter implements ICodeSubstituter {
|
||||||
|
constructor(
|
||||||
|
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
|
||||||
|
private readonly date = new Date(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
|
if (!code) { throw new Error('undefined code'); }
|
||||||
|
if (!info) { throw new Error('undefined info'); }
|
||||||
|
const args = new FunctionCallArgumentCollection();
|
||||||
|
const substitute = (name: string, value: string) =>
|
||||||
|
args.addArgument(new FunctionCallArgument(name, value));
|
||||||
|
substitute('homepage', info.homepage);
|
||||||
|
substitute('version', info.version);
|
||||||
|
substitute('date', this.date.toUTCString());
|
||||||
|
const compiledCode = this.compiler.compileExpressions(code, args);
|
||||||
|
return compiledCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||||
|
const parsers = [ new ParameterSubstitutionParser() ];
|
||||||
|
const parser = new CompositeExpressionParser(parsers);
|
||||||
|
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||||
|
return expressionCompiler;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
|
||||||
|
export interface ICodeSubstituter {
|
||||||
|
substitute(code: string, info: IProjectInformation): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { createEnumParser } from '../../Common/Enum';
|
||||||
|
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
import { CodeSubstituter } from './CodeSubstituter';
|
||||||
|
|
||||||
|
export class ScriptingDefinitionParser {
|
||||||
|
constructor(
|
||||||
|
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
||||||
|
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
public parse(
|
||||||
|
definition: ScriptingDefinitionData,
|
||||||
|
info: IProjectInformation): IScriptingDefinition {
|
||||||
|
if (!info) { throw new Error('undefined info'); }
|
||||||
|
if (!definition) { throw new Error('undefined definition'); }
|
||||||
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
|
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||||
|
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||||
|
return new ScriptingDefinition(
|
||||||
|
language,
|
||||||
|
startCode,
|
||||||
|
endCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
|
||||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
|
||||||
import { createEnumParser } from '../Common/Enum';
|
|
||||||
import { generateIlCode } from './Script/Compiler/ILCode';
|
|
||||||
|
|
||||||
export function parseScriptingDefinition(
|
|
||||||
definition: ScriptingDefinitionData,
|
|
||||||
info: IProjectInformation,
|
|
||||||
date = new Date(),
|
|
||||||
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
|
|
||||||
if (!info) {
|
|
||||||
throw new Error('undefined info');
|
|
||||||
}
|
|
||||||
if (!definition) {
|
|
||||||
throw new Error('undefined definition');
|
|
||||||
}
|
|
||||||
const language = languageParser.parseEnum(definition.language, 'language');
|
|
||||||
const startCode = applySubstitutions(definition.startCode, info, date);
|
|
||||||
const endCode = applySubstitutions(definition.endCode, info, date);
|
|
||||||
return new ScriptingDefinition(
|
|
||||||
language,
|
|
||||||
startCode,
|
|
||||||
endCode,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
|
|
||||||
let ilCode = generateIlCode(code);
|
|
||||||
ilCode = ilCode.substituteParameter('homepage', info.homepage);
|
|
||||||
ilCode = ilCode.substituteParameter('version', info.version);
|
|
||||||
ilCode = ilCode.substituteParameter('date', date.toUTCString());
|
|
||||||
return ilCode.compile();
|
|
||||||
}
|
|
||||||
40
src/application/collections/collection.yaml.d.ts
vendored
40
src/application/collections/collection.yaml.d.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
declare module 'js-yaml-loader!*' {
|
declare module 'js-yaml-loader!@/*' {
|
||||||
export interface CollectionData {
|
export interface CollectionData {
|
||||||
readonly os: string;
|
readonly os: string;
|
||||||
readonly scripting: ScriptingDefinitionData;
|
readonly scripting: ScriptingDefinitionData;
|
||||||
@@ -18,30 +18,38 @@ declare module 'js-yaml-loader!*' {
|
|||||||
readonly docs?: DocumentationUrlsData;
|
readonly docs?: DocumentationUrlsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionData {
|
export interface InstructionHolder {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code: string;
|
|
||||||
revertCode?: string;
|
readonly code?: string;
|
||||||
parameters?: readonly string[];
|
readonly revertCode?: string;
|
||||||
|
|
||||||
|
readonly call?: FunctionCallsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParameterDefinitionData {
|
||||||
|
readonly name: string;
|
||||||
|
readonly optional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionData extends InstructionHolder {
|
||||||
|
readonly parameters?: readonly ParameterDefinitionData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionCallParametersData {
|
export interface FunctionCallParametersData {
|
||||||
[index: string]: string;
|
readonly [index: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionCallData {
|
export interface FunctionCallData {
|
||||||
function: string;
|
readonly function: string;
|
||||||
parameters?: FunctionCallParametersData;
|
readonly parameters?: FunctionCallParametersData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
export interface ScriptData extends DocumentableData {
|
export interface ScriptData extends InstructionHolder, DocumentableData {
|
||||||
name: string;
|
readonly name: string;
|
||||||
code?: string;
|
readonly recommend?: string;
|
||||||
revertCode?: string;
|
|
||||||
call: ScriptFunctionCallData;
|
|
||||||
recommend?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { getEnumNames, getEnumValues } from '@/application/Common/Enum';
|
import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
@@ -21,7 +21,7 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
throw new Error('undefined scripting definition');
|
throw new Error('undefined scripting definition');
|
||||||
}
|
}
|
||||||
this.queryable = makeQueryable(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
ensureValidOs(os);
|
assertInRange(os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
ensureNoDuplicates(this.queryable.allCategories);
|
ensureNoDuplicates(this.queryable.allCategories);
|
||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
@@ -54,18 +54,6 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidOs(os: OperatingSystem): void {
|
|
||||||
if (os === undefined) {
|
|
||||||
throw new Error('undefined os');
|
|
||||||
}
|
|
||||||
if (os === OperatingSystem.Unknown) {
|
|
||||||
throw new Error('unknown os');
|
|
||||||
}
|
|
||||||
if (!(os in OperatingSystem)) {
|
|
||||||
throw new Error(`os "${os}" is out of range`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||||
const totalOccurrencesById = new Map<TKey, number>();
|
const totalOccurrencesById = new Map<TKey, number>();
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ export enum OperatingSystem {
|
|||||||
Android,
|
Android,
|
||||||
iOS,
|
iOS,
|
||||||
WindowsPhone,
|
WindowsPhone,
|
||||||
Unknown,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IProjectInformation } from './IProjectInformation';
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
|
|
||||||
export class ProjectInformation implements IProjectInformation {
|
export class ProjectInformation implements IProjectInformation {
|
||||||
public readonly repositoryWebUrl: string;
|
public readonly repositoryWebUrl: string;
|
||||||
@@ -42,6 +43,7 @@ function getWebUrl(gitUrl: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFileName(os: OperatingSystem, version: string): string {
|
function getFileName(os: OperatingSystem, version: string): string {
|
||||||
|
assertInRange(os, OperatingSystem);
|
||||||
switch (os) {
|
switch (os) {
|
||||||
case OperatingSystem.Linux:
|
case OperatingSystem.Linux:
|
||||||
return `privacy.sexy-${version}.AppImage`;
|
return `privacy.sexy-${version}.AppImage`;
|
||||||
@@ -50,6 +52,6 @@ function getFileName(os: OperatingSystem, version: string): string {
|
|||||||
case OperatingSystem.Windows:
|
case OperatingSystem.Windows:
|
||||||
return `privacy.sexy-Setup-${version}.exe`;
|
return `privacy.sexy-Setup-${version}.exe`;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
|
throw new RangeError(`Unsupported os: ${OperatingSystem[os]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,10 @@ export class ScriptCode implements IScriptCode {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string,
|
||||||
scriptName: string,
|
|
||||||
syntax: ILanguageSyntax) {
|
syntax: ILanguageSyntax) {
|
||||||
if (!scriptName) { throw new Error('script name is undefined'); }
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
if (!syntax) { throw new Error('syntax is undefined'); }
|
validateCode(execute, syntax);
|
||||||
validateCode(scriptName, execute, syntax);
|
validateRevertCode(revert, execute, syntax);
|
||||||
if (revert) {
|
|
||||||
scriptName = `${scriptName} (revert)`;
|
|
||||||
validateCode(scriptName, revert, syntax);
|
|
||||||
if (execute === revert) {
|
|
||||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,32 +16,60 @@ export interface ILanguageSyntax {
|
|||||||
readonly commonCodeParts: string[];
|
readonly commonCodeParts: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
|
||||||
if (!code || code.length === 0) {
|
if (!revertCode) {
|
||||||
throw new Error(`code of ${name} is empty or undefined`);
|
|
||||||
}
|
|
||||||
ensureNoEmptyLines(name, code);
|
|
||||||
ensureCodeHasUniqueLines(name, code, syntax);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(name: string, code: string): void {
|
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`script has empty lines "${name}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
|
||||||
const lines = code.split('\n')
|
|
||||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
try {
|
||||||
if (duplicateLines.length !== 0) {
|
validateCode(revertCode, syntax);
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
if (execute === revertCode) {
|
||||||
|
throw new Error(`Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw Error(`(revert): ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCode(code: string, syntax: ILanguageSyntax): void {
|
||||||
|
if (!code || code.length === 0) {
|
||||||
|
throw new Error(`code is empty or undefined`);
|
||||||
|
}
|
||||||
|
ensureNoEmptyLines(code);
|
||||||
|
ensureCodeHasUniqueLines(code, syntax);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoEmptyLines(code: string): void {
|
||||||
|
const lines = code.split(/\r\n|\r|\n/);
|
||||||
|
if (lines.some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`Script has empty lines:\n${lines.map((part, index) => `\n (${index}) ${part || '❌'}`).join('')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodeHasUniqueLines(code: string, syntax: ILanguageSyntax): void {
|
||||||
|
const allLines = code.split(/\r\n|\r|\n/);
|
||||||
|
const checkedLines = allLines.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||||
|
if (checkedLines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateLines = checkedLines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
|
if (duplicateLines.length !== 0) {
|
||||||
|
throw Error(`Duplicates detected in script:\n${printDuplicatedLines(allLines)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printDuplicatedLines(allLines: string[]) {
|
||||||
|
return allLines
|
||||||
|
.map((line, index) => {
|
||||||
|
const occurrenceIndices = allLines
|
||||||
|
.map((e, i) => e === line ? i : '')
|
||||||
|
.filter(String);
|
||||||
|
const isDuplicate = occurrenceIndices.length > 1;
|
||||||
|
const indicator = isDuplicate ? `❌ (${occurrenceIndices.join(',')})\t` : '✅ ';
|
||||||
|
return `${indicator}[${index}] ${line}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
codeLine = codeLine.toLowerCase();
|
codeLine = codeLine.toLowerCase();
|
||||||
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export class Clipboard {
|
export class Clipboard {
|
||||||
public static copyText(text: string): void {
|
public static copyText(text: string): void {
|
||||||
const el = document.createElement('textarea');
|
const el = document.createElement('textarea');
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user