Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d328f08952 | ||
|
|
bcad357017 | ||
|
|
9845a7cd68 | ||
|
|
7c632f7388 | ||
|
|
1442f62633 | ||
|
|
7f7a84e3ba | ||
|
|
dee3279f85 | ||
|
|
094dbb01b8 | ||
|
|
e299d40fa1 | ||
|
|
cb42f11b97 | ||
|
|
4531645b4c | ||
|
|
bf3426f91b | ||
|
|
3864f04218 | ||
|
|
e541a35e86 | ||
|
|
bd383ed273 | ||
|
|
949fac1a7c | ||
|
|
7ab16ecccb | ||
|
|
58cd551a30 | ||
|
|
7770a9b521 | ||
|
|
aab0f7ea46 | ||
|
|
ea41f4f503 | ||
|
|
af7219f6e1 | ||
|
|
8ccaec7af6 | ||
|
|
b2ffc90da7 | ||
|
|
72e4d0b896 | ||
|
|
5bb13e34f8 | ||
|
|
0466b86f10 | ||
|
|
ca81f68ff1 | ||
|
|
4995e49c46 | ||
|
|
77123d8c92 | ||
|
|
e72c1c13ea | ||
|
|
e775d68a9b | ||
|
|
f8e5f1a5a2 | ||
|
|
f4a74f058d | ||
|
|
80821fca07 | ||
|
|
dfd4451561 | ||
|
|
8570b02dde | ||
|
|
d6da406c61 | ||
|
|
060e789662 | ||
|
|
e40b9a3cf5 | ||
|
|
237d9944f9 |
@@ -10,7 +10,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
// Vue specific rules, eslint-plugin-vue
|
// Vue specific rules, eslint-plugin-vue
|
||||||
'plugin:vue/essential',
|
'plugin:vue/vue3-recommended',
|
||||||
|
|
||||||
// Extends eslint-config-airbnb
|
// Extends eslint-config-airbnb
|
||||||
'@vue/eslint-config-airbnb-with-typescript',
|
'@vue/eslint-config-airbnb-with-typescript',
|
||||||
|
|||||||
2
.github/workflows/release.site.yaml
vendored
2
.github/workflows/release.site.yaml
vendored
@@ -102,7 +102,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: "App: Deploy to S3"
|
name: "App: Deploy to S3"
|
||||||
shell: bash
|
shell: bash
|
||||||
run: >-
|
run: |-
|
||||||
declare web_output_dir
|
declare web_output_dir
|
||||||
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
if ! web_output_dir=$(cd app && node scripts/print-dist-dir.js --web); then
|
||||||
echo 'Error: Could not determine distribution directory.'
|
echo 'Error: Could not determine distribution directory.'
|
||||||
|
|||||||
38
.github/workflows/tests.e2e.yaml
vendored
38
.github/workflows/tests.e2e.yaml
vendored
@@ -24,3 +24,41 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run e2e tests
|
name: Run e2e tests
|
||||||
run: npm run test:cy:run
|
run: npm run test:cy:run
|
||||||
|
-
|
||||||
|
name: Output artifact directories
|
||||||
|
id: artifacts
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
declare -r dirs_json_file='cypress-dirs.json'
|
||||||
|
if [ ! -f "${dirs_json_file}" ]; then
|
||||||
|
echo "${dirs_json_file} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}")
|
||||||
|
VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}")
|
||||||
|
|
||||||
|
for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do
|
||||||
|
if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then
|
||||||
|
echo "One or more directories are null or not specified in cypress-dirs.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
-
|
||||||
|
name: Upload screenshots
|
||||||
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
|
-
|
||||||
|
name: Upload videos
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-videos-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,5 +1,13 @@
|
|||||||
node_modules
|
# Application build artifacts
|
||||||
/dist-*/
|
/dist-*/
|
||||||
.vs
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
.vscode/**/*
|
.vscode/**/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# draw.io
|
||||||
|
*.bkp
|
||||||
|
*.dtmp
|
||||||
|
|||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.7 (2023-11-07)
|
||||||
|
|
||||||
|
* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98)
|
||||||
|
* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1)
|
||||||
|
* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7)
|
||||||
|
|
||||||
|
## 0.12.6 (2023-11-03)
|
||||||
|
|
||||||
|
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
||||||
|
* win: improve system app uninstall cleanup #73 | [dbe3c5c](https://github.com/undergroundwires/privacy.sexy/commit/dbe3c5cfb91ba8a1657838b69117858843c8fbc8)
|
||||||
|
* win: improve system app uninstall /w fallback #260 | [98a26f9](https://github.com/undergroundwires/privacy.sexy/commit/98a26f9ae47af2668aa53f39d1768983036048ce)
|
||||||
|
* Improve performance of rendering during search | [79b46bf](https://github.com/undergroundwires/privacy.sexy/commit/79b46bf21004d96d31551439e5db5d698a3f71f3)
|
||||||
|
* Fix YAML error for site release in CI/CD | [237d994](https://github.com/undergroundwires/privacy.sexy/commit/237d9944f900f5172366868d75219224ff0542b0)
|
||||||
|
* win: fix Microsoft Advertising app removal #200 | [e40b9a3](https://github.com/undergroundwires/privacy.sexy/commit/e40b9a3cf53c341f2e84023a9f0e9680ac08f3fa)
|
||||||
|
* win: improve directory cleanup security | [060e789](https://github.com/undergroundwires/privacy.sexy/commit/060e7896624309aebd25e8b190c127282de177e8)
|
||||||
|
* Centralize Electron entry file path configuration | [d6da406](https://github.com/undergroundwires/privacy.sexy/commit/d6da406c61e5b9f5408851d1302d6d7398157a2e)
|
||||||
|
* win: prevent updates from reinstalling apps #260 | [8570b02](https://github.com/undergroundwires/privacy.sexy/commit/8570b02dde14ffad64863f614682c3fc1f87b6c2)
|
||||||
|
* win: improve script environment robustness #221 | [dfd4451](https://github.com/undergroundwires/privacy.sexy/commit/dfd44515613f38abe5a806bda36f44e7b715b50b)
|
||||||
|
* Fix compiler failing with nested `with` expression | [80821fc](https://github.com/undergroundwires/privacy.sexy/commit/80821fca0769e5fd2c6338918fbdcea12fbe83d2)
|
||||||
|
* win: improve soft file/app delete security #260 | [f4a74f0](https://github.com/undergroundwires/privacy.sexy/commit/f4a74f058db9b5bcbcbe438785db5ec88ecc1657)
|
||||||
|
* Fix incorrect tooltip position after window resize | [f8e5f1a](https://github.com/undergroundwires/privacy.sexy/commit/f8e5f1a5a2afa1f18567e6d965359b6a1f082367)
|
||||||
|
* linux: fix string formatting of Firefox configs | [e775d68](https://github.com/undergroundwires/privacy.sexy/commit/e775d68a9b4a5f9e893ff0e3500dade036185193)
|
||||||
|
* win: improve file delete | [e72c1c1](https://github.com/undergroundwires/privacy.sexy/commit/e72c1c13ea2d73ebfc7a8da5a21254fdfc0e5b59)
|
||||||
|
* win: change system app removal to hard delete #260 | [77123d8](https://github.com/undergroundwires/privacy.sexy/commit/77123d8c929d23676a9cb21d7b697703fd1b6e82)
|
||||||
|
* Improve UI performance by optimizing reactivity | [4995e49](https://github.com/undergroundwires/privacy.sexy/commit/4995e49c469211404dac9fcb79b75eb121f80bce)
|
||||||
|
* Migrate to Vue 3.0 #230 | [ca81f68](https://github.com/undergroundwires/privacy.sexy/commit/ca81f68ff1c3bbe5b22981096ae9220b0b5851c7)
|
||||||
|
* win, linux: unify & improve Firefox clean-up #273 | [0466b86](https://github.com/undergroundwires/privacy.sexy/commit/0466b86f1013341c966a9bbf6513990337b16598)
|
||||||
|
* win: fix store revert for multiple installs #260 | [5bb13e3](https://github.com/undergroundwires/privacy.sexy/commit/5bb13e34f8de2e2a7ba943ff72b12c0569435e62)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.5...0.12.6)
|
||||||
|
|
||||||
## 0.12.5 (2023-10-13)
|
## 0.12.5 (2023-10-13)
|
||||||
|
|
||||||
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
* Fix Docker build and improve checks #220 | [7669985](https://github.com/undergroundwires/privacy.sexy/commit/7669985f8e1446e726a95626ecf35b3ce6b60a16)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ You have two alternatives:
|
|||||||
|
|
||||||
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
1. [Create an issue](https://github.com/undergroundwires/privacy.sexy/issues/new/choose) and ask for someone else to add the script for you.
|
||||||
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
2. Or send a PR yourself. This would make it faster to get your code into the project. You need to add scripts to related OS in [collections](src/application/collections/) folder. Then you'd sent a pull request, see [pull request process](#pull-request-process).
|
||||||
|
- 💡 You should use existing shared functions for most of the operations, like `DisableService` for disabling services, to maintain code consistency and efficiency.
|
||||||
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
- 📖 If you're unsure about the syntax, check [collection-files.md](docs/collection-files.md).
|
||||||
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
- 📖 If you wish to use templates, use [templating.md](./docs/templating.md).
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-Setup-0.12.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.5/privacy.sexy-0.12.5.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-Setup-0.12.7.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
@@ -153,13 +153,22 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
## Additional Install Options
|
## Additional Install Options
|
||||||
|
|
||||||
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
- Check the [releases page](https://github.com/undergroundwires/privacy.sexy/releases) for all available versions.
|
||||||
- Using [Scoop](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) package manager on Windows:
|
- Other unofficial channels (not maintained by privacy.sexy) for Windows include:
|
||||||
|
- [Scoop 🥄](https://scoop.sh/#/apps?q=privacy.sexy&s=2&d=1&o=true) (latest version):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
scoop bucket add extras
|
scoop bucket add extras
|
||||||
scoop install privacy.sexy
|
scoop install privacy.sexy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- [winget 🪟](https://winget.run/pkg/undergroundwires/privacy.sexy) (may be outdated):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install -e --id undergroundwires.privacy.sexy
|
||||||
|
```
|
||||||
|
|
||||||
|
With winget, updates require manual submission; the auto-update feature within privacy.sexy will notify you of new releases post-installation.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
Refer to [development.md](./docs/development.md) for Docker usage and reading more about setting up your development environment.
|
||||||
|
|||||||
5
cypress-dirs.json
Normal file
5
cypress-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"base": "tests/e2e",
|
||||||
|
"videos": "tests/e2e/videos",
|
||||||
|
"screenshots": "tests/e2e/videos"
|
||||||
|
}
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
import ViteConfig from './vite.config';
|
import ViteConfig from './vite.config';
|
||||||
|
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
|
||||||
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
fixturesFolder: `${cypressDirs.base}/fixtures`,
|
||||||
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
screenshotsFolder: cypressDirs.screenshots,
|
||||||
|
|
||||||
video: true,
|
video: true,
|
||||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
videosFolder: cypressDirs.videos,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
baseUrl: `http://localhost:${getApplicationPort()}/`,
|
||||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
supportFile: `${cypressDirs.base}/support/e2e.ts`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than
|
||||||
|
`cy.get()`. It bypasses the usual same-origin policy constraints
|
||||||
|
*/
|
||||||
|
chromeWebSecurity: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getApplicationPort(): number {
|
||||||
|
const port = ViteConfig.server?.port;
|
||||||
|
if (port === undefined) {
|
||||||
|
throw new Error('Unknown application port');
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
|
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
@@ -20,8 +22,7 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
@@ -70,10 +71,11 @@ To add a new dependency:
|
|||||||
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
||||||
- **Singletons**: Shared across components, instantiated once.
|
- **Singletons**: Shared across components, instantiated once.
|
||||||
- **Transients**: Factories yielding a new instance on every access.
|
- **Transients**: Factories yielding a new instance on every access.
|
||||||
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
2. **Provide the dependency**:
|
||||||
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
|
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency.
|
||||||
- For singletons, invoke the factory method: `inject(symbolKey)()`.
|
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
||||||
- For transients, directly inject: `inject(symbolKey)`.
|
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies.
|
||||||
|
- Example usage: `injectKey((keys) => keys.useCollectionState)`;
|
||||||
|
|
||||||
## Shared UI components
|
## Shared UI components
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,69 @@
|
|||||||
|
|
||||||
## Benefits of templating
|
## Benefits of templating
|
||||||
|
|
||||||
- Generating scripts by sharing code to increase best-practice usage and maintainability.
|
- **Code sharing:** Share code across scripts for consistent practices and easier maintenance.
|
||||||
- Creating self-contained scripts without cross-dependencies.
|
- **Script independence:** Generate self-contained scripts, eliminating the need for external code.
|
||||||
- Use of pipes for writing cleaner code and letting pipes do dirty work.
|
- **Cleaner code:** Use pipes for complex operations, resulting in more readable and streamlined code.
|
||||||
|
|
||||||
## Expressions
|
## Expressions
|
||||||
|
|
||||||
- Expressions start and end with mustaches (double brackets, `{{` and `}}`).
|
**Syntax:**
|
||||||
- E.g. `Hello {{ $name }} !`
|
|
||||||
- Syntax is close to [Go Templates ❤️](https://pkg.go.dev/text/template) but not the same.
|
Expressions are enclosed within `{{` and `}}`.
|
||||||
- Functions enables usage of expressions.
|
Example: `Hello {{ $name }}!`.
|
||||||
- In script definition parts of a function, see [`Function`](./collection-files.md#Function).
|
They are a core component of templating, enhancing scripts with dynamic capabilities and functionality.
|
||||||
- When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function).
|
|
||||||
- Expressions inside expressions (nested templates) are supported.
|
**Syntax similarity:**
|
||||||
- An expression can output another expression that will also be compiled.
|
|
||||||
- E.g. following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output.
|
The syntax shares similarities with [Go Templates ❤️](https://pkg.go.dev/text/template), but with some differences:
|
||||||
|
|
||||||
|
**Function definitions:**
|
||||||
|
|
||||||
|
You can use expressions in function definition.
|
||||||
|
Refer to [Function](./collection-files.md#function) for more details.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: GreetFunction
|
||||||
|
parameters:
|
||||||
|
- name: name
|
||||||
|
code: Hello {{ $name }}!
|
||||||
|
```
|
||||||
|
|
||||||
|
If you assign `name` the value `world`, invoking `GreetFunction` would result in `Hello world!`.
|
||||||
|
|
||||||
|
**Function arguments:**
|
||||||
|
|
||||||
|
You can also use expressions in arguments in nested function calls.
|
||||||
|
Refer to [`Function | collection-files.md`](./collection-files.md#functioncall) for more details.
|
||||||
|
|
||||||
|
Example with nested function calls:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
-
|
||||||
|
name: PrintMessageFunction
|
||||||
|
parameters:
|
||||||
|
- name: message
|
||||||
|
code: echo "{{ $message }}"
|
||||||
|
-
|
||||||
|
name: GreetUserFunction
|
||||||
|
parameters:
|
||||||
|
- name: userName
|
||||||
|
call:
|
||||||
|
name: PrintMessageFunction
|
||||||
|
parameters:
|
||||||
|
argument: 'Hello, {{ $userName }}!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, if `userName` is `Alice`, invoking `GreetUserFunction` would execute `echo "Hello, Alice!"`.
|
||||||
|
|
||||||
|
**Nested templates:**
|
||||||
|
|
||||||
|
You can nest expressions inside expressions (also called "nested templates").
|
||||||
|
This means that an expression can output another expression where compiler will compile both.
|
||||||
|
|
||||||
|
For example, following would compile first [with expression](#with), and then [parameter substitution](#parameter-substitution) in its output:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -26,55 +74,70 @@
|
|||||||
|
|
||||||
### Parameter substitution
|
### Parameter substitution
|
||||||
|
|
||||||
A simple function example:
|
Parameter substitution dynamically replaces variable references with their corresponding values in the script.
|
||||||
|
|
||||||
|
**Example function:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
function: EchoArgument
|
name: DisplayTextFunction
|
||||||
parameters:
|
parameters:
|
||||||
- name: 'argument'
|
- name: 'text'
|
||||||
code: Hello {{ $argument }} !
|
code: echo {{ $text }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It would print "Hello world" if it's called in a [script](./collection-files.md#script) as following:
|
Invoking `DisplayTextFunction` with `text` set to `"Hello, world!"` would result in `echo "Hello, World!"`.
|
||||||
|
|
||||||
```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
|
### with
|
||||||
|
|
||||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
The `with` expression enables conditional rendering and provides a context variable for simpler code.
|
||||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
|
||||||
|
|
||||||
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
**Optional block rendering:**
|
||||||
|
|
||||||
|
If the provided variable is falsy (`false`, `null`, or empty), the compiler skips the enclosed block of code.
|
||||||
|
A "block" lies between the with start (`{{ with .. }}`) and end (`{{ end }}`) expressions, defining its boundaries.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $optionalVariable }}
|
||||||
|
Hello
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would display `Hello` if `$optionalVariable` is truthy.
|
||||||
|
|
||||||
|
**Parameter declaration:**
|
||||||
|
|
||||||
|
You should set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||||
|
|
||||||
|
Declare parameters used for `with` condition as optional such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: ConditionalOutputFunction
|
||||||
|
parameters:
|
||||||
|
- name: 'data'
|
||||||
|
optional: true
|
||||||
|
code: |-
|
||||||
|
{{ with $data }}
|
||||||
|
Data is: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Context variable:**
|
||||||
|
|
||||||
|
`with` statement binds its context (value of the provided parameter value) as arbitrary `.` value.
|
||||||
|
`{{ . }}` syntax gives you access to the context variable.
|
||||||
|
This is optional to use, and not required to use `with` expressions.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
It supports multiline text inside the block. You can have something like:
|
**Multiline text:**
|
||||||
|
|
||||||
|
It supports multiline text inside the block. You can write something like:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $argument }}
|
{{ with $argument }}
|
||||||
@@ -83,7 +146,9 @@ It supports multiline text inside the block. You can have something like:
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
**Inner expressions:**
|
||||||
|
|
||||||
|
You can also embed other expressions inside its block, such as [parameter substitution](#parameter-substitution):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
{{ with $condition }}
|
{{ with $condition }}
|
||||||
@@ -91,32 +156,44 @@ You can also use other expressions inside its block, such as [parameter substitu
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
This also includes nesting `with` statements:
|
||||||
|
|
||||||
Example:
|
```go
|
||||||
|
{{ with $condition1 }}
|
||||||
```yaml
|
Value of $condition1: {{ . }}
|
||||||
function: FunctionThatOutputsConditionally
|
{{ with $condition2 }}
|
||||||
parameters:
|
Value of $condition2: {{ . }}
|
||||||
- name: 'argument'
|
{{ end }}
|
||||||
optional: true
|
|
||||||
code: |-
|
|
||||||
{{ with $argument }}
|
|
||||||
Value is: {{ . }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pipes
|
### Pipes
|
||||||
|
|
||||||
- Pipes are functions available for handling text.
|
Pipes are functions designed for text manipulation.
|
||||||
- Allows stacking actions one after another also known as "chaining".
|
They allow for a sequential application of operations resembling [Unix pipelines](https://en.wikipedia.org/wiki/Pipeline_(Unix)), also known as "chaining".
|
||||||
- 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.
|
Each pipeline's output becomes the input of the following pipe.
|
||||||
- You cannot create pipes. [A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
|
||||||
- You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
**Pre-defined**:
|
||||||
- ❗ Pipe names must be camelCase without any space or special characters.
|
|
||||||
- **Existing pipes**
|
Pipes are pre-defined by the system.
|
||||||
|
You cannot create pipes in [collection files](./collection-files.md).
|
||||||
|
[A dedicated compiler](./application.md#parsing-and-compiling) provides pre-defined pipes to consume in collection files.
|
||||||
|
|
||||||
|
**Compatibility:**
|
||||||
|
|
||||||
|
You can combine pipes with other expressions such as [parameter substitution](#parameter-substitution) and [with](#with) syntax.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
{{ with $script }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Naming:**
|
||||||
|
|
||||||
|
❗ Pipe names must be camelCase without any space or special characters.
|
||||||
|
|
||||||
|
**Available pipes:**
|
||||||
|
|
||||||
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
- `inlinePowerShell`: Converts a multi-lined PowerShell script to a single line.
|
||||||
- `escapeDoubleQuotes`: Escapes `"` characters, allows you to use them inside double quotes (`"`).
|
- `escapeDoubleQuotes`: Escapes `"` characters for batch command execution, allows you to use them inside double quotes (`"`).
|
||||||
- **Example usages**
|
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell }}" {{ end }}`
|
|
||||||
- `{{ with $code }} echo "{{ . | inlinePowerShell | escapeDoubleQuotes }}" {{ end }}`
|
|
||||||
|
|||||||
@@ -68,21 +68,23 @@ These checks validate various qualities like runtime execution, building process
|
|||||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
|
||||||
- [`./tests/unit/`](./../tests/unit/)
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
- Stores unit test code.
|
- Stores unit test code.
|
||||||
- The directory structure mirrors [`./src/`](./../src).
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
- [`shared/`](./../tests/unit/shared/)
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
- Contains shared unit test functionalities.
|
- Contains shared unit test functionalities.
|
||||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
|
||||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
- Shared test cases.
|
- Shared test cases.
|
||||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||||
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||||
|
- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||||
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
|
||||||
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||||
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file.
|
||||||
|
- [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability.
|
||||||
|
|||||||
@@ -8,15 +8,21 @@ import distDirs from './dist-dirs.json' assert { type: 'json' };
|
|||||||
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
const MAIN_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/main/index.ts');
|
||||||
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
const PRELOAD_ENTRY_FILE = resolvePathFromProjectRoot('src/presentation/electron/preload/index.ts');
|
||||||
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
const WEB_INDEX_HTML_PATH = resolvePathFromProjectRoot('src/presentation/index.html');
|
||||||
const DIST_DIR = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
const ELECTRON_DIST_SUBDIRECTORIES = {
|
||||||
|
main: resolveElectronDistSubdirectory('main'),
|
||||||
|
preload: resolveElectronDistSubdirectory('preload'),
|
||||||
|
renderer: resolveElectronDistSubdirectory('renderer'),
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
distDirSubfolder: 'main',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.main,
|
||||||
entryFilePath: MAIN_ENTRY_FILE,
|
entryFilePath: MAIN_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
preload: getSharedElectronConfig({
|
preload: getSharedElectronConfig({
|
||||||
distDirSubfolder: 'preload',
|
distDirSubfolder: ELECTRON_DIST_SUBDIRECTORIES.preload,
|
||||||
entryFilePath: PRELOAD_ENTRY_FILE,
|
entryFilePath: PRELOAD_ENTRY_FILE,
|
||||||
}),
|
}),
|
||||||
renderer: mergeConfig(
|
renderer: mergeConfig(
|
||||||
@@ -25,7 +31,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, 'renderer'),
|
outDir: ELECTRON_DIST_SUBDIRECTORIES.renderer,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: WEB_INDEX_HTML_PATH,
|
index: WEB_INDEX_HTML_PATH,
|
||||||
@@ -42,7 +48,7 @@ function getSharedElectronConfig(options: {
|
|||||||
}): UserConfig {
|
}): UserConfig {
|
||||||
return {
|
return {
|
||||||
build: {
|
build: {
|
||||||
outDir: resolve(DIST_DIR, options.distDirSubfolder),
|
outDir: options.distDirSubfolder,
|
||||||
lib: {
|
lib: {
|
||||||
entry: options.entryFilePath,
|
entry: options.entryFilePath,
|
||||||
},
|
},
|
||||||
@@ -64,6 +70,11 @@ function getSharedElectronConfig(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePathFromProjectRoot(pathSegment: string) {
|
function resolvePathFromProjectRoot(pathSegment: string): string {
|
||||||
return resolve(__dirname, pathSegment);
|
return resolve(__dirname, pathSegment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveElectronDistSubdirectory(subDirectory: string): string {
|
||||||
|
const electronDistDir = resolvePathFromProjectRoot(distDirs.electronUnbundled);
|
||||||
|
return resolve(electronDistDir, subDirectory);
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
1183
package-lock.json
generated
1183
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist-electron-unbundled/main/index.cjs",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
@@ -25,7 +24,7 @@
|
|||||||
"electron:preview": "electron-vite preview",
|
"electron:preview": "electron-vite preview",
|
||||||
"electron:prebuild": "electron-vite build",
|
"electron:prebuild": "electron-vite build",
|
||||||
"electron:build": "electron-builder",
|
"electron:build": "electron-builder",
|
||||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
"electron-updater": "^6.1.4",
|
"electron-updater": "^6.1.4",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^13.0.2",
|
||||||
"vue": "^2.7.14"
|
"vue": "^3.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
"@modyfi/vite-plugin-yaml": "^1.0.4",
|
||||||
@@ -53,17 +52,17 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.62.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vitejs/plugin-vue2": "^2.2.0",
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^7.0.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/test-utils": "^1.3.6",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"cypress": "^13.3.1",
|
"cypress": "^13.3.1",
|
||||||
"electron": "^27.0.0",
|
"electron": "^27.0.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^1.0.27",
|
"electron-vite": "^1.0.28",
|
||||||
"eslint": "^8.51.0",
|
"eslint": "^8.51.0",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
@@ -98,5 +97,11 @@
|
|||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"dmg-license": "^1.0.11"
|
||||||
|
},
|
||||||
|
"//optionalDependencies": {
|
||||||
|
"dmg-license": "Required by `electron-builder` for DMG builds on macOS, https://github.com/electron-userland/electron-builder/issues/6489, https://github.com/electron-userland/electron-builder/issues/6520"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
|
|||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetterType) {
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
|||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Environment = {
|
interface ErrorPrototypeManipulation {
|
||||||
|
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
|
||||||
|
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
|
||||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fixPrototype(target: Error, prototype: CustomError) {
|
function fixPrototype(target: Error, prototype: CustomError) {
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
// This is recommended by TypeScript guidelines.
|
||||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
if (!functionExists(setPrototypeOf)) {
|
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
|
||||||
|
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
|
||||||
|
if (!isFunction(setPrototypeOf)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPrototypeOf(target, prototype);
|
setPrototypeOf(target, prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStackTrace(target: Error) {
|
function ensureStackTrace(target: Error) {
|
||||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
|
||||||
if (!functionExists(captureStackTrace)) {
|
if (!isFunction(captureStackTrace)) {
|
||||||
// captureStackTrace is only available on V8, if it's not available
|
// captureStackTrace is only available on V8, if it's not available
|
||||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||||
return;
|
return;
|
||||||
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
|
|||||||
captureStackTrace(target, target.constructor);
|
captureStackTrace(target, target.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function functionExists(func: unknown): boolean {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
function isFunction(func: unknown): func is Function {
|
||||||
return typeof func === 'function';
|
return typeof func === 'function';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
) {
|
) {
|
||||||
if (value === undefined || value === null) {
|
|
||||||
throw new Error('absent enum value');
|
|
||||||
}
|
|
||||||
if (!(value in enumVariable)) {
|
if (!(value in enumVariable)) {
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
|
|
||||||
public create(language: ScriptingLanguage): T {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
const getter = this.getters.get(language);
|
||||||
|
if (!getter) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
}
|
}
|
||||||
const getter = this.getters.get(language);
|
|
||||||
const instance = getter();
|
const instance = getter();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
if (this.getters.has(language)) {
|
if (this.getters.has(language)) {
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
import { TimeoutType, Timer } from './Timer';
|
||||||
|
|
||||||
|
export function batchedDebounce<T>(
|
||||||
|
callback: (batches: readonly T[]) => void,
|
||||||
|
waitInMs: number,
|
||||||
|
timer: Timer = PlatformTimer,
|
||||||
|
): (arg: T) => void {
|
||||||
|
let lastTimeoutId: TimeoutType | undefined;
|
||||||
|
let batches: Array<T> = [];
|
||||||
|
|
||||||
|
return (arg: T) => {
|
||||||
|
batches.push(arg);
|
||||||
|
|
||||||
|
const later = () => {
|
||||||
|
callback(batches);
|
||||||
|
batches = [];
|
||||||
|
lastTimeoutId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastTimeoutId !== undefined) {
|
||||||
|
timer.clearTimeout(lastTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimeoutId = timer.setTimeout(later, waitInMs);
|
||||||
|
};
|
||||||
|
}
|
||||||
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Timer } from './Timer';
|
||||||
|
|
||||||
|
export const PlatformTimer: Timer = {
|
||||||
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
||||||
|
dateNow: () => Date.now(),
|
||||||
|
};
|
||||||
@@ -1,47 +1,29 @@
|
|||||||
|
import { Timer, TimeoutType } from './Timer';
|
||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
|
||||||
export type CallbackType = (..._: unknown[]) => void;
|
export type CallbackType = (..._: unknown[]) => void;
|
||||||
|
|
||||||
export function throttle(
|
export function throttle(
|
||||||
callback: CallbackType,
|
callback: CallbackType,
|
||||||
waitInMs: number,
|
waitInMs: number,
|
||||||
timer: ITimer = NodeTimer,
|
timer: Timer = PlatformTimer,
|
||||||
): CallbackType {
|
): CallbackType {
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
const throttler = new Throttler(timer, waitInMs, callback);
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
class Throttler {
|
||||||
export type TimeoutType = ReturnType<typeof setTimeout>;
|
private queuedExecutionId: TimeoutType | undefined;
|
||||||
|
|
||||||
export interface ITimer {
|
|
||||||
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
|
||||||
clearTimeout: (timeoutId: TimeoutType) => void;
|
|
||||||
dateNow(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeTimer: ITimer = {
|
|
||||||
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
||||||
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
|
||||||
dateNow: () => Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IThrottler {
|
|
||||||
invoke: CallbackType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Throttler implements IThrottler {
|
|
||||||
private queuedExecutionId: TimeoutType;
|
|
||||||
|
|
||||||
private previouslyRun: number;
|
private previouslyRun: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: ITimer,
|
private readonly timer: Timer,
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
) {
|
) {
|
||||||
if (!timer) { throw new Error('missing timer'); }
|
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
if (!callback) { throw new Error('missing callback'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
8
src/application/Common/Timing/Timer.ts
Normal file
8
src/application/Common/Timing/Timer.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
||||||
|
export type TimeoutType = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
export interface Timer {
|
||||||
|
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
||||||
|
clearTimeout: (timeoutId: TimeoutType) => void;
|
||||||
|
dateNow(): number;
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
validateApp(app);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collection = this.app.getCollection(os);
|
const collection = this.app.getCollection(os);
|
||||||
if (!this.collection) {
|
this.collection = collection;
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
|
||||||
}
|
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.states[os],
|
newState: this.states[os],
|
||||||
oldState: this.states[this.currentOs],
|
oldState: this.states[this.currentOs],
|
||||||
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
|
||||||
if (!app) {
|
|
||||||
throw new Error('missing app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -10,18 +10,23 @@ export async function buildContext(
|
|||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment.os);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
function getInitialOs(
|
||||||
|
app: IApplication,
|
||||||
|
currentOs: OperatingSystem | undefined,
|
||||||
|
): OperatingSystem {
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
|
return getMostSupportedOs(supportedOsList, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
|
|||||||
@@ -4,23 +4,48 @@ import { UserFilter } from './Filter/UserFilter';
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
public readonly code: IApplicationCode;
|
public readonly code: IApplicationCode;
|
||||||
|
|
||||||
public readonly selection: IUserSelection;
|
public readonly selection: UserSelection;
|
||||||
|
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public constructor(
|
||||||
this.selection = new UserSelection(collection, []);
|
public readonly collection: ICategoryCollection,
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
selectionFactory = DefaultSelectionFactory,
|
||||||
this.filter = new UserFilter(collection);
|
codeFactory = DefaultCodeFactory,
|
||||||
|
filterFactory = DefaultFilterFactory,
|
||||||
|
) {
|
||||||
|
this.selection = selectionFactory(collection, []);
|
||||||
|
this.code = codeFactory(this.selection.scripts, collection.scripting);
|
||||||
|
this.filter = filterFactory(collection);
|
||||||
this.os = collection.os;
|
this.os = collection.os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodeFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ApplicationCode>
|
||||||
|
) => IApplicationCode;
|
||||||
|
|
||||||
|
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
|
||||||
|
|
||||||
|
export type SelectionFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserSelectionFacade>
|
||||||
|
) => UserSelection;
|
||||||
|
|
||||||
|
const DefaultSelectionFactory: SelectionFactory = (
|
||||||
|
...params
|
||||||
|
) => new UserSelectionFacade(...params);
|
||||||
|
|
||||||
|
export type FilterFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserFilter>
|
||||||
|
) => IUserFilter;
|
||||||
|
|
||||||
|
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
@@ -17,15 +17,12 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IReadOnlyUserSelection,
|
selection: ReadonlyScriptSelection,
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
) {
|
) {
|
||||||
if (!userSelection) { throw new Error('missing userSelection'); }
|
this.setCode(selection.selectedScripts);
|
||||||
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
selection.changed.on((scripts) => {
|
||||||
if (!generator) { throw new Error('missing generator'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
|
||||||
userSelection.changed.on((scripts) => {
|
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
@@ -36,7 +36,18 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
return this.scripts.get(script);
|
return this.getPositionById(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
|
const position = [...this.scripts.entries()]
|
||||||
|
.filter(([s]) => s.id === scriptId)
|
||||||
|
.map(([, pos]) => pos)
|
||||||
|
.at(0);
|
||||||
|
if (!position) {
|
||||||
|
throw new Error('Unknown script: Position could not be found for the script');
|
||||||
|
}
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePo
|
|||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
addedScripts: ReadonlyArray<IScript>;
|
readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
removedScripts: ReadonlyArray<IScript>;
|
readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
changedScripts: ReadonlyArray<IScript>;
|
readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
if (lines) {
|
||||||
this.lines.push(...lines);
|
this.lines.push(...lines);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
code: string;
|
readonly code: string;
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
readonly scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('missing scripts'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
@@ -68,8 +66,19 @@ function appendSelection(
|
|||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const { script } = selection;
|
const { script } = selection;
|
||||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
const scriptCode = getSelectedCode(selection);
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedCode(selection: SelectedScript): string {
|
||||||
|
const { code } = selection.script;
|
||||||
|
if (!selection.revert) {
|
||||||
|
return code.execute;
|
||||||
|
}
|
||||||
|
if (!code.revert) {
|
||||||
|
throw new Error('Reverted script lacks revert code.');
|
||||||
|
}
|
||||||
|
return code.revert;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
import {
|
||||||
|
IFilterChangeDetails, IFilterChangeDetailsVisitor,
|
||||||
|
ApplyFilterAction, ClearFilterAction,
|
||||||
|
} from './IFilterChangeDetails';
|
||||||
|
|
||||||
export class FilterChange implements IFilterChangeDetails {
|
export class FilterChange implements IFilterChangeDetails {
|
||||||
public static forApply(filter: IFilterResult) {
|
public static forApply(
|
||||||
if (!filter) {
|
filter: IFilterResult,
|
||||||
throw new Error('missing filter');
|
): IFilterChangeDetails {
|
||||||
}
|
return new FilterChange({ type: FilterActionType.Apply, filter });
|
||||||
return new FilterChange(FilterActionType.Apply, filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forClear() {
|
public static forClear(): IFilterChangeDetails {
|
||||||
return new FilterChange(FilterActionType.Clear);
|
return new FilterChange({ type: FilterActionType.Clear });
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
|
||||||
public readonly actionType: FilterActionType,
|
|
||||||
public readonly filter?: IFilterResult,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
if (!visitor) {
|
switch (this.action.type) {
|
||||||
throw new Error('missing visitor');
|
|
||||||
}
|
|
||||||
switch (this.actionType) {
|
|
||||||
case FilterActionType.Apply:
|
case FilterActionType.Apply:
|
||||||
visitor.onApply(this.filter);
|
if (visitor.onApply) {
|
||||||
|
visitor.onApply(this.action.filter);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FilterActionType.Clear:
|
case FilterActionType.Clear:
|
||||||
|
if (visitor.onClear) {
|
||||||
visitor.onClear();
|
visitor.onClear();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
throw new Error(`Unknown action: ${this.action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
|
|
||||||
export interface IFilterChangeDetails {
|
export interface IFilterChangeDetails {
|
||||||
readonly actionType: FilterActionType;
|
readonly action: FilterAction;
|
||||||
readonly filter?: IFilterResult;
|
|
||||||
|
|
||||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilterChangeDetailsVisitor {
|
export interface IFilterChangeDetailsVisitor {
|
||||||
onClear(): void;
|
readonly onClear?: () => void;
|
||||||
onApply(filter: IFilterResult): void;
|
readonly onApply?: (filter: IFilterResult) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApplyFilterAction = {
|
||||||
|
readonly type: FilterActionType.Apply,
|
||||||
|
readonly filter: IFilterResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearFilterAction = {
|
||||||
|
readonly type: FilterActionType.Clear,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterAction = ApplyFilterAction | ClearFilterAction;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
|
|||||||
public readonly query: string,
|
public readonly query: string,
|
||||||
) {
|
) {
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface IReadOnlyCategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
readonly filter: IReadOnlyUserFilter;
|
readonly filter: IReadOnlyUserFilter;
|
||||||
readonly selection: IReadOnlyUserSelection;
|
readonly selection: ReadonlyUserSelection;
|
||||||
readonly collection: ICategoryCollection;
|
readonly collection: ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: UserSelection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyCategorySelection {
|
||||||
|
areAllScriptsSelected(category: ICategory): boolean;
|
||||||
|
isAnyScriptSelected(category: ICategory): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||||
|
processChanges(action: CategorySelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type CategorySelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategorySelectionChange {
|
||||||
|
readonly categoryId: number;
|
||||||
|
readonly newStatus: CategorySelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelectionChangeCommand {
|
||||||
|
readonly changes: readonly CategorySelectionChange[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
|
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
||||||
|
import { CategorySelection } from './CategorySelection';
|
||||||
|
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||||
|
constructor(
|
||||||
|
private readonly scriptSelection: ScriptSelection,
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public areAllScriptsSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every(
|
||||||
|
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAnyScriptSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
|
public processChanges(action: CategorySelectionChangeCommand): void {
|
||||||
|
const scriptChanges = action.changes.reduce((changes, change) => {
|
||||||
|
changes.push(...this.collectScriptChanges(change));
|
||||||
|
return changes;
|
||||||
|
}, new Array<ScriptSelectionChange>());
|
||||||
|
this.scriptSelection.processChanges({
|
||||||
|
changes: scriptChanges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
|
||||||
|
const category = this.collection.getCategory(change.categoryId);
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
const scriptsChangesInCategory = scripts
|
||||||
|
.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
...change.newStatus,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return scriptsChangesInCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export interface IReadOnlyUserSelection {
|
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
|
||||||
isSelected(scriptId: string): boolean;
|
|
||||||
areAllSelected(category: ICategory): boolean;
|
|
||||||
isAnySelected(category: ICategory): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUserSelection extends IReadOnlyUserSelection {
|
|
||||||
removeAllInCategory(categoryId: number): void;
|
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
removeSelectedScript(scriptId: string): void;
|
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
|
||||||
selectAll(): void;
|
|
||||||
deselectAll(): void;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
|
import { ScriptSelection } from './ScriptSelection';
|
||||||
|
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
|
|
||||||
|
const DEBOUNCE_DELAY_IN_MS = 100;
|
||||||
|
|
||||||
|
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
|
||||||
|
|
||||||
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
|
private readonly scripts: Repository<string, SelectedScript>;
|
||||||
|
|
||||||
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
|
) {
|
||||||
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
|
for (const script of selectedScripts) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
|
}
|
||||||
|
this.processChanges = debounce(
|
||||||
|
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
|
||||||
|
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
|
||||||
|
this.processScriptChanges(consolidatedChanges);
|
||||||
|
},
|
||||||
|
DEBOUNCE_DELAY_IN_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSelected(scriptId: string): boolean {
|
||||||
|
return this.scripts.exists(scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
|
return this.scripts.getItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectAll(): void {
|
||||||
|
const scriptsToSelect = this.collection
|
||||||
|
.getAllScripts()
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
|
if (scriptsToSelect.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public deselectAll(): void {
|
||||||
|
if (this.scripts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
|
this.processChanges({
|
||||||
|
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: [
|
||||||
|
...getScriptIdsToBeDeselected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...getScriptIdsToBeSelected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
|
||||||
|
let totalChanged = 0;
|
||||||
|
for (const change of changes) {
|
||||||
|
totalChanged += this.applyChange(change);
|
||||||
|
}
|
||||||
|
if (totalChanged > 0) {
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
|
const script = this.collection.getScript(change.scriptId);
|
||||||
|
if (change.newStatus.isSelected) {
|
||||||
|
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||||
|
}
|
||||||
|
return this.removeScript(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
|
const script = this.collection.getScript(scriptId);
|
||||||
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const existingSelectedScript = this.scripts.getById(selectedScript.id);
|
||||||
|
if (equals(selectedScript, existingSelectedScript)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeScript(scriptId: string): number {
|
||||||
|
if (!this.scripts.exists(scriptId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.removeItem(scriptId);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeSelected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return desiredScripts
|
||||||
|
.filter((script) => !existingItems.exists(script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeDeselected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return existingItems
|
||||||
|
.getItems()
|
||||||
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
|
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyScriptSelection {
|
||||||
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
|
isSelected(scriptId: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
|
selectOnly(scripts: readonly IScript[]): void;
|
||||||
|
selectAll(): void;
|
||||||
|
deselectAll(): void;
|
||||||
|
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type ScriptSelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ScriptSelectionChange {
|
||||||
|
readonly scriptId: string;
|
||||||
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelectionChangeCommand {
|
||||||
|
readonly changes: ReadonlyArray<ScriptSelectionChange>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
type ScriptId = IScript['id'];
|
||||||
|
|
||||||
|
export interface SelectedScript extends IEntity<ScriptId> {
|
||||||
|
readonly script: IScript;
|
||||||
|
readonly revert: boolean;
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class SelectedScript extends BaseEntity<string> {
|
type SelectedScriptId = SelectedScript['id'];
|
||||||
|
|
||||||
|
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error('cannot revert an irreversible script');
|
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,167 +1,12 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export interface ReadonlyUserSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
readonly categories: ReadonlyCategorySelection;
|
||||||
|
readonly scripts: ReadonlyScriptSelection;
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly collection: ICategoryCollection,
|
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
|
||||||
) {
|
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
|
||||||
for (const script of selectedScripts) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
export interface UserSelection extends ReadonlyUserSelection {
|
||||||
if (this.selectedScripts.length === 0) {
|
readonly categories: CategorySelection;
|
||||||
return false;
|
readonly scripts: ScriptSelection;
|
||||||
}
|
|
||||||
const scripts = category.getAllScriptsRecursively();
|
|
||||||
if (this.selectedScripts.length < scripts.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return scripts.every(
|
|
||||||
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
|
||||||
if (this.selectedScripts.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
|
||||||
const category = this.collection.findCategory(categoryId);
|
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
|
||||||
if (!scriptsToRemove.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToRemove) {
|
|
||||||
this.scripts.removeItem(script.id);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
|
||||||
const scriptsToAddOrUpdate = this.collection
|
|
||||||
.findCategory(categoryId)
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.filter(
|
|
||||||
(script) => !this.scripts.exists(script.id)
|
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|
||||||
)
|
|
||||||
.map((script) => new SelectedScript(script, revert));
|
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
|
||||||
this.scripts.addOrUpdateItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
if (!script) {
|
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
|
||||||
}
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
|
||||||
return this.scripts.exists(scriptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
|
||||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
|
||||||
return this.scripts.getItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
|
||||||
const scriptsToSelect = this.collection
|
|
||||||
.getAllScripts()
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
if (scriptsToSelect.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToSelect) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectAll(): void {
|
|
||||||
if (this.scripts.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
|
||||||
for (const scriptId of selectedScriptIds) {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
}
|
|
||||||
this.changed.notify([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
|
||||||
if (!scripts || scripts.length === 0) {
|
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
|
||||||
}
|
|
||||||
let totalChanged = 0;
|
|
||||||
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
|
||||||
totalChanged += this.selectNewWithoutNotifying(scripts);
|
|
||||||
if (totalChanged > 0) {
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
if (this.scripts.length === 0 || scripts.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const existingItems = this.scripts.getItems();
|
|
||||||
const missingIds = existingItems
|
|
||||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
|
||||||
.map((script) => script.id);
|
|
||||||
for (const id of missingIds) {
|
|
||||||
this.scripts.removeItem(id);
|
|
||||||
}
|
|
||||||
return missingIds.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
const unselectedScripts = scripts
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
for (const newScript of unselectedScripts) {
|
|
||||||
this.scripts.addItem(newScript);
|
|
||||||
}
|
|
||||||
return unselectedScripts.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { CategorySelection } from './Category/CategorySelection';
|
||||||
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
|
import { ScriptSelection } from './Script/ScriptSelection';
|
||||||
|
import { UserSelection } from './UserSelection';
|
||||||
|
import { SelectedScript } from './Script/SelectedScript';
|
||||||
|
|
||||||
|
export class UserSelectionFacade implements UserSelection {
|
||||||
|
public readonly categories: CategorySelection;
|
||||||
|
|
||||||
|
public readonly scripts: ScriptSelection;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
collection: ICategoryCollection,
|
||||||
|
selectedScripts: readonly SelectedScript[],
|
||||||
|
scriptsFactory = DefaultScriptsFactory,
|
||||||
|
categoriesFactory = DefaultCategoriesFactory,
|
||||||
|
) {
|
||||||
|
this.scripts = scriptsFactory(collection, selectedScripts);
|
||||||
|
this.categories = categoriesFactory(this.scripts, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptsFactory = (
|
||||||
|
...params: ConstructorParameters<typeof DebouncedScriptSelection>
|
||||||
|
) => ScriptSelection;
|
||||||
|
|
||||||
|
const DefaultScriptsFactory: ScriptsFactory = (
|
||||||
|
...params
|
||||||
|
) => new DebouncedScriptSelection(...params);
|
||||||
|
|
||||||
|
export type CategoriesFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
|
||||||
|
) => CategorySelection;
|
||||||
|
|
||||||
|
const DefaultCategoriesFactory: CategoriesFactory = (
|
||||||
|
...params
|
||||||
|
) => new ScriptToCategorySelectionMapper(...params);
|
||||||
@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections?.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
|
||||||
throw new Error('missing collection provided');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ export function parseCategoryCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content.actions.length) {
|
||||||
throw new Error('missing content');
|
|
||||||
}
|
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
CategoryData, ScriptData, CategoryOrScriptData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
@@ -16,7 +16,6 @@ export function parseCategory(
|
|||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
|
|||||||
readonly factory: CategoryFactoryType,
|
readonly factory: CategoryFactoryType,
|
||||||
readonly parentCategory?: CategoryData,
|
readonly parentCategory?: CategoryData,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
|||||||
/* scripts: */ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new NodeValidator({
|
return new NodeValidator({
|
||||||
type: NodeType.Category,
|
type: NodeType.Category,
|
||||||
selfNode: context.categoryData,
|
selfNode: context.categoryData,
|
||||||
parentNode: context.parentCategory,
|
parentNode: context.parentCategory,
|
||||||
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
|
|||||||
.assertDefined(category)
|
.assertDefined(category)
|
||||||
.assertValidName(category.category)
|
.assertValidName(category.category)
|
||||||
.assert(
|
.assert(
|
||||||
() => category.children && category.children.length > 0,
|
() => category.children.length > 0,
|
||||||
`"${category.category}" has no children.`,
|
`"${category.category}" has no children.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
|
|||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData as CategoryData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
factory: context.factory,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subCategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else if (isScript(context.nodeData)) {
|
||||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
const script = parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
validator.throw('Node is neither a category or a script.');
|
validator.throw('Node is neither a category or a script.');
|
||||||
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
const holder = (data as InstructionHolder);
|
return hasCode(data) || hasCall(data);
|
||||||
return hasCode(holder) || hasCall(holder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return hasProperty(data, 'category');
|
return hasProperty(data, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: InstructionHolder): boolean {
|
function hasCode(data: unknown): boolean {
|
||||||
return hasProperty(data, 'code');
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: InstructionHolder) {
|
function hasCall(data: unknown) {
|
||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
|
||||||
throw new Error('missing documentable');
|
|
||||||
}
|
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class NodeValidator {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public throw(errorMessage: string) {
|
public throw(errorMessage: string): never {
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
|||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
) {
|
) {
|
||||||
if (!scripting) { throw new Error('missing scripting'); }
|
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ export class Expression implements IExpression {
|
|||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
) {
|
) {
|
||||||
if (!position) {
|
|
||||||
throw new Error('missing position');
|
|
||||||
}
|
|
||||||
if (!evaluator) {
|
|
||||||
throw new Error('missing evaluator');
|
|
||||||
}
|
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
|
||||||
throw new Error('missing context');
|
|
||||||
}
|
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
|||||||
@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
|||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
) {
|
) {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
|
export function createPositionFromRegexFullMatch(
|
||||||
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
if (startPos === undefined) {
|
||||||
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
if (!fullMatch.length) {
|
||||||
|
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const endPos = startPos + fullMatch.length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
public compileExpressions(
|
public compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): string {
|
): string {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return '';
|
||||||
}
|
}
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): void {
|
): void {
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
if (!usedParameterNames?.length) {
|
if (!usedParameterNames.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notProvidedParameters = usedParameterNames
|
const notProvidedParameters = usedParameterNames
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
|||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ const Parsers = [
|
|||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (!leafs) {
|
if (!leafs.length) {
|
||||||
throw new Error('missing leafs');
|
throw new Error('missing leafs');
|
||||||
}
|
}
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
|
||||||
throw new Error('missing leaf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
|||||||
@@ -14,45 +14,44 @@ export class ExpressionRegexBuilder {
|
|||||||
.addRawRegex('\\s+');
|
.addRawRegex('\\s+');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchPipeline() {
|
public captureOptionalPipeline() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.addRawRegex('((?:\\|\\s*\\b[a-zA-Z]+\\b\\s*)*)');
|
||||||
.addRawRegex('(\\|\\s*.+?)?');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchUntilFirstWhitespace() {
|
public captureUntilWhitespaceOrPipe() {
|
||||||
return this
|
return this
|
||||||
.addRawRegex('([^|\\s]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
public captureMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.addRawRegex('([\\S\\s]+?)')
|
.addRawRegex('([\\s\\S]*\\S)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionStart() {
|
public expectExpressionStart() {
|
||||||
return this
|
return this
|
||||||
.expectCharacters('{{')
|
.expectCharacters('{{')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectOptionalWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
public expectExpressionEnd() {
|
public expectExpressionEnd() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectOptionalWhitespaces()
|
||||||
.expectCharacters('}}');
|
.expectCharacters('}}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public expectOptionalWhitespaces() {
|
||||||
|
return this
|
||||||
|
.addRawRegex('\\s*');
|
||||||
|
}
|
||||||
|
|
||||||
public buildRegExp(): RegExp {
|
public buildRegExp(): RegExp {
|
||||||
return new RegExp(this.parts.join(''), 'g');
|
return new RegExp(this.parts.join(''), 'g');
|
||||||
}
|
}
|
||||||
|
|
||||||
private expectZeroOrMoreWhitespaces() {
|
|
||||||
return this
|
|
||||||
.addRawRegex('\\s*');
|
|
||||||
}
|
|
||||||
|
|
||||||
private addRawRegex(regex: string) {
|
private addRawRegex(regex: string) {
|
||||||
this.parts.push(regex);
|
this.parts.push(regex);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionParser } from '../IExpressionParser';
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
|
||||||
import { IExpression } from '../../Expression/IExpression';
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
const matches = code.matchAll(this.regex);
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||||
const parameters = createParameters(primitiveExpression);
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
yield expression;
|
||||||
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: IPrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
): FunctionParameterCollection {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function hasLines(text: string) {
|
|||||||
*/
|
*/
|
||||||
function inlineComments(code: string): string {
|
function inlineComments(code: string): string {
|
||||||
const makeInlineComment = (comment: string) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = comment?.trim();
|
const value = comment.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
if (!pipes) {
|
|
||||||
throw new Error('missing pipes');
|
|
||||||
}
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
|
||||||
throw new Error('missing pipe in list');
|
|
||||||
}
|
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
if (!this.pipes.has(pipeName)) {
|
const pipe = this.pipes.get(pipeName);
|
||||||
|
if (!pipe) {
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
}
|
}
|
||||||
return this.pipes.get(pipeName);
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
protected readonly regex = new ExpressionRegexBuilder()
|
protected readonly regex = new ExpressionRegexBuilder()
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('$')
|
.expectCharacters('$')
|
||||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
.captureUntilWhitespaceOrPipe() // First capture: Parameter name
|
||||||
.matchPipeline() // Second match: Pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // Second capture: Pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,220 @@
|
|||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export class WithParser extends RegexParser {
|
export class WithParser implements IExpressionParser {
|
||||||
protected readonly regex = new ExpressionRegexBuilder()
|
public findExpressions(code: string): IExpression[] {
|
||||||
// {{ with $parameterName }}
|
if (!code) {
|
||||||
.expectExpressionStart()
|
throw new Error('missing code');
|
||||||
.expectCharacters('with')
|
}
|
||||||
.expectOneOrMoreWhitespaces()
|
return parseWithExpressions(code);
|
||||||
.expectCharacters('$')
|
}
|
||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
}
|
||||||
.expectExpressionEnd()
|
|
||||||
// ...
|
|
||||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
|
||||||
// {{ end }}
|
|
||||||
.expectExpressionStart()
|
|
||||||
.expectCharacters('end')
|
|
||||||
.expectExpressionEnd()
|
|
||||||
.buildRegExp();
|
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
enum WithStatementType {
|
||||||
const parameterName = match[1];
|
Start,
|
||||||
const scopeText = match[2];
|
End,
|
||||||
|
ContextVariable,
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithStatement = {
|
||||||
|
readonly type: WithStatementType.Start;
|
||||||
|
readonly parameterName: string;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
} | {
|
||||||
|
readonly type: WithStatementType.End;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
} | {
|
||||||
|
readonly type: WithStatementType.ContextVariable;
|
||||||
|
readonly position: ExpressionPosition;
|
||||||
|
readonly pipeline: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseAllWithExpressions(
|
||||||
|
input: string,
|
||||||
|
): WithStatement[] {
|
||||||
|
const expressions = new Array<WithStatement>();
|
||||||
|
for (const match of input.matchAll(WithStatementStartRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.Start,
|
||||||
|
parameterName: match[1],
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.End,
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
||||||
|
expressions.push({
|
||||||
|
type: WithStatementType.ContextVariable,
|
||||||
|
position: createPositionFromRegexFullMatch(match),
|
||||||
|
pipeline: match[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
class WithStatementBuilder {
|
||||||
|
private readonly contextVariables = new Array<{
|
||||||
|
readonly positionInScope: ExpressionPosition;
|
||||||
|
readonly pipeline: string | undefined;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
public addContextVariable(
|
||||||
|
absolutePosition: ExpressionPosition,
|
||||||
|
pipeline: string | undefined,
|
||||||
|
): void {
|
||||||
|
const positionInScope = new ExpressionPosition(
|
||||||
|
absolutePosition.start - this.startExpressionPosition.end,
|
||||||
|
absolutePosition.end - this.startExpressionPosition.end,
|
||||||
|
);
|
||||||
|
this.contextVariables.push({
|
||||||
|
positionInScope,
|
||||||
|
pipeline,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
|
||||||
|
const parameters = new FunctionParameterCollection();
|
||||||
|
parameters.addParameter(new FunctionParameter(this.parameterName, true));
|
||||||
|
const position = new ExpressionPosition(
|
||||||
|
this.startExpressionPosition.start,
|
||||||
|
endExpressionPosition.end,
|
||||||
|
);
|
||||||
|
const scope = input.substring(this.startExpressionPosition.end, endExpressionPosition.start);
|
||||||
return {
|
return {
|
||||||
parameters: [new FunctionParameter(parameterName, true)],
|
parameters,
|
||||||
evaluator: (context) => {
|
position,
|
||||||
const argumentValue = context.args.hasArgument(parameterName)
|
evaluate: (context) => {
|
||||||
? context.args.getArgument(parameterName).argumentValue
|
const argumentValue = context.args.hasArgument(this.parameterName)
|
||||||
|
? context.args.getArgument(this.parameterName).argumentValue
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
const substitutedScope = this.substituteContextVariables(scope, (pipeline) => {
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
return argumentValue;
|
return argumentValue;
|
||||||
}
|
}
|
||||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||||
});
|
});
|
||||||
|
return substitutedScope;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly startExpressionPosition: ExpressionPosition,
|
||||||
|
private readonly parameterName: string,
|
||||||
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
private substituteContextVariables(
|
||||||
|
scope: string,
|
||||||
|
substituter: (pipeline?: string) => string,
|
||||||
|
): string {
|
||||||
|
if (!this.contextVariables.length) {
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
let substitutedScope = '';
|
||||||
|
let scopeSubstrIndex = 0;
|
||||||
|
for (const contextVariable of this.contextVariables) {
|
||||||
|
substitutedScope += scope.substring(scopeSubstrIndex, contextVariable.positionInScope.start);
|
||||||
|
substitutedScope += substituter(contextVariable.pipeline);
|
||||||
|
scopeSubstrIndex = contextVariable.positionInScope.end;
|
||||||
|
}
|
||||||
|
substitutedScope += scope.substring(scopeSubstrIndex, scope.length);
|
||||||
|
return substitutedScope;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildErrorContext(code: string, statements: readonly WithStatement[]): string {
|
||||||
|
const formattedStatements = statements.map((s) => `- [${s.position.start}, ${s.position.end}] ${WithStatementType[s.type]}`).join('\n');
|
||||||
|
return [
|
||||||
|
'Code:', '---', code, '---',
|
||||||
|
'nStatements:', '---', formattedStatements, '---',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWithExpressions(input: string): IExpression[] {
|
||||||
|
const allStatements = parseAllWithExpressions(input);
|
||||||
|
const sortedStatements = allStatements
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
|
const expressions = new Array<IExpression>();
|
||||||
|
const builders = new Array<WithStatementBuilder>();
|
||||||
|
const throwWithContext = (message: string): never => {
|
||||||
|
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
||||||
|
};
|
||||||
|
while (sortedStatements.length > 0) {
|
||||||
|
const statement = sortedStatements.pop();
|
||||||
|
if (!statement) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (statement.type) { // eslint-disable-line default-case
|
||||||
|
case WithStatementType.Start:
|
||||||
|
builders.push(new WithStatementBuilder(
|
||||||
|
statement.position,
|
||||||
|
statement.parameterName,
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case WithStatementType.ContextVariable:
|
||||||
|
if (builders.length === 0) {
|
||||||
|
throwWithContext('Context variable before `with` statement.');
|
||||||
|
}
|
||||||
|
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
||||||
|
break;
|
||||||
|
case WithStatementType.End: {
|
||||||
|
const builder = builders.pop();
|
||||||
|
if (!builder) {
|
||||||
|
throwWithContext('Redundant `end` statement, missing `with`?');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
expressions.push(builder.buildExpression(statement.position, input));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (builders.length > 0) {
|
||||||
|
throwWithContext('Missing `end` statement, forgot `{{ end }}?');
|
||||||
|
}
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextVariableWithPipelineRegEx = new ExpressionRegexBuilder()
|
||||||
// {{ . | pipeName }}
|
// {{ . | pipeName }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('.')
|
.expectCharacters('.')
|
||||||
.matchPipeline() // First match: pipeline
|
.expectOptionalWhitespaces()
|
||||||
|
.captureOptionalPipeline() // First capture: pipeline
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
const WithStatementStartRegEx = new ExpressionRegexBuilder()
|
||||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
// {{ with $parameterName }}
|
||||||
// but instead letting the pipeline compiler to fail on those.
|
.expectExpressionStart()
|
||||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
.expectCharacters('with')
|
||||||
return replacer(match1);
|
.expectOneOrMoreWhitespaces()
|
||||||
});
|
.expectCharacters('$')
|
||||||
}
|
.captureUntilWhitespaceOrPipe() // First capture: parameter name
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.buildRegExp();
|
||||||
|
|
||||||
|
const WithStatementEndRegEx = new ExpressionRegexBuilder()
|
||||||
|
// {{ end }}
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionStart()
|
||||||
|
.expectCharacters('end')
|
||||||
|
.expectOptionalWhitespaces()
|
||||||
|
.expectExpressionEnd()
|
||||||
|
.buildRegExp();
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
|
||||||
throw new Error('missing argument');
|
|
||||||
}
|
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
|
|||||||
|
|
||||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||||
if (!codeSegments?.length) {
|
if (!codeSegments.length) {
|
||||||
throw new Error('missing segments');
|
throw new Error('missing segments');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
revertCode: joinCodeParts(
|
||||||
|
codeSegments
|
||||||
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||||
return codeSegments
|
return codeSegments
|
||||||
.filter((segment) => segment?.length > 0)
|
.filter((segment) => segment.length > 0)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
|||||||
calls: readonly FunctionCall[],
|
calls: readonly FunctionCall[],
|
||||||
functions: ISharedFunctionCollection,
|
functions: ISharedFunctionCollection,
|
||||||
): CompiledCode {
|
): CompiledCode {
|
||||||
if (!functions) { throw new Error('missing functions'); }
|
if (!calls.length) { throw new Error('missing calls'); }
|
||||||
if (!calls?.length) { throw new Error('missing calls'); }
|
|
||||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
|
||||||
const context: FunctionCallCompilationContext = {
|
const context: FunctionCallCompilationContext = {
|
||||||
allFunctions: functions,
|
allFunctions: functions,
|
||||||
rootCallSequence: calls,
|
rootCallSequence: calls,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.code !== undefined;
|
return func.body.type === FunctionBodyType.Code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
calledFunction: ISharedFunction,
|
calledFunction: ISharedFunction,
|
||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
|
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||||
|
throw new Error([
|
||||||
|
'Unexpected function body type.',
|
||||||
|
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||||
|
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||||
|
'Function:',
|
||||||
|
`\t${JSON.stringify(callToFunction)}`,
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
const { code } = calledFunction.body;
|
const { code } = calledFunction.body;
|
||||||
const { args } = callToFunction;
|
const { args } = callToFunction;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
revertCode: (() => {
|
||||||
|
if (!code.revert) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.expressionsCompiler.compileExpressions(code.revert, args);
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.calls !== undefined;
|
return func.body.type === FunctionBodyType.Calls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
context: FunctionCallCompilationContext,
|
context: FunctionCallCompilationContext,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
const nestedCalls = calledFunction.body.calls;
|
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
|
||||||
return nestedCalls.map((nestedCall) => {
|
return nestedCalls.map((nestedCall) => {
|
||||||
try {
|
try {
|
||||||
const compiledParentCall = this.argumentCompiler
|
const compiledParentCall = this.argumentCompiler
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
|||||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||||
|
|
||||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||||
if (calls === undefined) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const sequence = getCallSequence(calls);
|
const sequence = getCallSequence(calls);
|
||||||
return sequence.map((call) => parseFunctionCall(call));
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
}
|
}
|
||||||
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
|||||||
if (calls instanceof Array) {
|
if (calls instanceof Array) {
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
return [calls as FunctionCallData];
|
const singleCall = calls;
|
||||||
|
return [singleCall];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||||
if (!call) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const callArgs = parseArgs(call.parameters);
|
const callArgs = parseArgs(call.parameters);
|
||||||
return new ParsedFunctionCall(call.function, callArgs);
|
return new ParsedFunctionCall(call.function, callArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(
|
function parseArgs(
|
||||||
parameters: FunctionCallParametersData,
|
parameters: FunctionCallParametersData | undefined,
|
||||||
): FunctionCallArgumentCollection {
|
): FunctionCallArgumentCollection {
|
||||||
return Object.keys(parameters || {})
|
const parametersMap = parameters ?? {};
|
||||||
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
return Object.keys(parametersMap)
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
|
||||||
.reduce((args, arg) => {
|
.reduce((args, arg) => {
|
||||||
args.addArgument(arg);
|
args.addArgument(arg);
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
|
|||||||
if (!functionName) {
|
if (!functionName) {
|
||||||
throw new Error('missing function name in function call');
|
throw new Error('missing function name in function call');
|
||||||
}
|
}
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
|
|||||||
export interface ISharedFunction {
|
export interface ISharedFunction {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
readonly body: ISharedFunctionBody;
|
readonly body: SharedFunctionBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISharedFunctionBody {
|
export interface CallFunctionBody {
|
||||||
readonly type: FunctionBodyType;
|
readonly type: FunctionBodyType.Calls,
|
||||||
readonly code: IFunctionCode | undefined;
|
readonly calls: readonly FunctionCall[],
|
||||||
readonly calls: readonly FunctionCall[] | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeFunctionBody {
|
||||||
|
readonly type: FunctionBodyType.Code;
|
||||||
|
readonly code: IFunctionCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
|
||||||
|
|
||||||
export enum FunctionBodyType {
|
export enum FunctionBodyType {
|
||||||
Code,
|
Code,
|
||||||
Calls,
|
Calls,
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
if (!parameter) {
|
|
||||||
throw new Error('missing parameter');
|
|
||||||
}
|
|
||||||
if (this.includesName(parameter.name)) {
|
if (this.includesName(parameter.name)) {
|
||||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FunctionCall } from './Call/FunctionCall';
|
import { FunctionCall } from './Call/FunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export function createCallerFunction(
|
|||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
callSequence: readonly FunctionCall[],
|
callSequence: readonly FunctionCall[],
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
if (!callSequence || !callSequence.length) {
|
if (!callSequence.length) {
|
||||||
throw new Error(`missing call sequence in function "${name}"`);
|
throw new Error(`missing call sequence in function "${name}"`);
|
||||||
}
|
}
|
||||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||||
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SharedFunction implements ISharedFunction {
|
class SharedFunction implements ISharedFunction {
|
||||||
public readonly body: ISharedFunctionBody;
|
public readonly body: SharedFunctionBody;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
|
|||||||
bodyType: FunctionBodyType,
|
bodyType: FunctionBodyType,
|
||||||
) {
|
) {
|
||||||
if (!name) { throw new Error('missing function name'); }
|
if (!name) { throw new Error('missing function name'); }
|
||||||
if (!parameters) { throw new Error('missing parameters'); }
|
|
||||||
|
switch (bodyType) {
|
||||||
|
case FunctionBodyType.Code:
|
||||||
this.body = {
|
this.body = {
|
||||||
type: bodyType,
|
type: FunctionBodyType.Code,
|
||||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
code: content as IFunctionCode,
|
||||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
|
||||||
};
|
};
|
||||||
|
break;
|
||||||
|
case FunctionBodyType.Calls:
|
||||||
|
this.body = {
|
||||||
|
type: FunctionBodyType.Calls,
|
||||||
|
calls: content as readonly FunctionCall[],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||||
|
|
||||||
public addFunction(func: ISharedFunction): void {
|
public addFunction(func: ISharedFunction): void {
|
||||||
if (!func) { throw new Error('missing function'); }
|
|
||||||
if (this.has(func.name)) {
|
if (this.has(func.name)) {
|
||||||
throw new Error(`function with name ${func.name} already exists`);
|
throw new Error(`function with name ${func.name} already exists`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type {
|
||||||
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
||||||
|
} from '@/application/collections/';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
@@ -55,11 +56,13 @@ function parseFunction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(
|
function validateCode(
|
||||||
data: FunctionData,
|
data: CodeFunctionData,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
validator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
): void {
|
): void {
|
||||||
[data.code, data.revertCode].forEach(
|
[data.code, data.revertCode]
|
||||||
|
.filter((code): code is string => Boolean(code))
|
||||||
|
.forEach(
|
||||||
(code) => validator.throwIfInvalid(
|
(code) => validator.throwIfInvalid(
|
||||||
code,
|
code,
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
|
|||||||
}, new FunctionParameterCollection());
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||||
return Boolean(data.code);
|
return (data as CodeInstruction).code !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: FunctionData): boolean {
|
function hasCall(data: FunctionData): data is CallFunctionData {
|
||||||
return Boolean(data.call);
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
ensureNoUndefinedItem(functions);
|
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
ensureExpectedParametersType(functions);
|
ensureExpectedParametersType(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
|
|||||||
return `"${list.join('","')}"`;
|
return `"${list.join('","')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||||
// Ensure functions do not define both call and code
|
// Ensure functions do not define both call and code
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
if (withBothCallAndCode.length) {
|
if (withBothCallAndCode.length) {
|
||||||
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
|
|||||||
&& value.every((item) => typeof item === 'object');
|
&& value.every((item) => typeof item === 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
function printNames(holders: readonly InstructionHolder[]) {
|
function printNames(holders: readonly FunctionData[]) {
|
||||||
return printList(holders.map((holder) => holder.name));
|
return printList(holders.map((holder) => holder.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
|
||||||
if (functions.some((func) => !func)) {
|
|
||||||
throw new Error('some functions are undefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
const duplicateCodes = getDuplicates(functions
|
const callFunctions = functions
|
||||||
|
.filter((func) => hasCode(func))
|
||||||
|
.map((func) => func as CodeFunctionData);
|
||||||
|
const duplicateCodes = getDuplicates(callFunctions
|
||||||
.map((func) => func.code)
|
.map((func) => func.code)
|
||||||
.filter((code) => code));
|
.filter((code) => code));
|
||||||
if (duplicateCodes.length > 0) {
|
if (duplicateCodes.length > 0) {
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
}
|
}
|
||||||
const duplicateRevertCodes = getDuplicates(functions
|
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||||
.filter((func) => func.revertCode)
|
.map((func) => func.revertCode)
|
||||||
.map((func) => func.revertCode));
|
.filter((code): code is string => Boolean(code)));
|
||||||
if (duplicateRevertCodes.length > 0) {
|
if (duplicateRevertCodes.length > 0) {
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script) { throw new Error('missing script'); }
|
return hasCall(script);
|
||||||
if (!script.call) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
if (!script) { throw new Error('missing script'); }
|
|
||||||
try {
|
try {
|
||||||
|
if (!hasCall(script)) {
|
||||||
|
throw new Error('Script does include any calls.');
|
||||||
|
}
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
validateCompiledCode(compiledCode, this.codeValidator);
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
[compiledCode.code, compiledCode.revertCode]
|
||||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
.filter((code): code is string => Boolean(code))
|
||||||
|
.map((code) => code as string)
|
||||||
|
.forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines()],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
||||||
|
return (data as CallInstruction).call !== undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
|
|||||||
import { CodeValidator } from './Validation/CodeValidator';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
@@ -24,7 +23,6 @@ export function parseScript(
|
|||||||
): Script {
|
): Script {
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = scriptFactory(
|
||||||
/* name: */ data.name,
|
/* name: */ data.name,
|
||||||
@@ -34,12 +32,12 @@ export function parseScript(
|
|||||||
);
|
);
|
||||||
return script;
|
return script;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
validator.throw(err.message);
|
return validator.throw(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string,
|
level: string | undefined,
|
||||||
parser: IEnumParser<RecommendationLevel>,
|
parser: IEnumParser<RecommendationLevel>,
|
||||||
): RecommendationLevel | undefined {
|
): RecommendationLevel | undefined {
|
||||||
if (!level) {
|
if (!level) {
|
||||||
@@ -56,39 +54,45 @@ function parseCode(
|
|||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const code = new ScriptCode(script.code, script.revertCode);
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
|
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: ScriptCode,
|
||||||
codeValidator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
[scriptCode.execute, scriptCode.revert].forEach(
|
[scriptCode.execute, scriptCode.revert]
|
||||||
(code) => codeValidator.throwIfInvalid(
|
.filter((code): code is string => Boolean(code))
|
||||||
|
.forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
code,
|
code,
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
function validateScript(
|
||||||
|
script: ScriptData,
|
||||||
|
validator: NodeValidator,
|
||||||
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator
|
validator
|
||||||
.assertDefined(script)
|
.assertDefined(script)
|
||||||
.assertValidName(script.name)
|
.assertValidName(script.name)
|
||||||
.assert(
|
.assert(
|
||||||
() => Boolean(script.code || script.call),
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
'Must define either "call" or "code".',
|
'Neither "call" or "code" is defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.code && script.call),
|
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||||
'Cannot define both "call" and "code".',
|
'Both "call" and "code" are defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.revertCode && script.call),
|
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||||
'Cannot define "revertCode" if "call" is defined.',
|
'Both "call" and "revertCode" are defined.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
|
|||||||
code: string,
|
code: string,
|
||||||
rules: readonly ICodeValidationRule[],
|
rules: readonly ICodeValidationRule[],
|
||||||
): void {
|
): void {
|
||||||
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
|
|||||||
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
export class NoDuplicatedLines implements ICodeValidationRule {
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
constructor(private readonly syntax: ILanguageSyntax) {
|
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
|
|
||||||
public substitute(code: string, info: IProjectInformation): string {
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
if (!code) { throw new Error('missing code'); }
|
if (!code) { throw new Error('missing code'); }
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
const args = new FunctionCallArgumentCollection();
|
const args = new FunctionCallArgumentCollection();
|
||||||
const substitute = (name: string, value: string) => args
|
const substitute = (name: string, value: string) => args
|
||||||
.addArgument(new FunctionCallArgument(name, value));
|
.addArgument(new FunctionCallArgument(name, value));
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
|
|||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
): IScriptingDefinition {
|
): IScriptingDefinition {
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
if (!definition) { throw new Error('missing definition'); }
|
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||||
|
|||||||
17
src/application/Repository/Repository.ts
Normal file
17
src/application/Repository/Repository.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
|
|
||||||
|
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
|
readonly length: number;
|
||||||
|
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
||||||
|
getById(id: TKey): TEntity;
|
||||||
|
exists(id: TKey): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
|
addItem(item: TEntity): void;
|
||||||
|
addOrUpdateItem(item: TEntity): void;
|
||||||
|
removeItem(id: TKey): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repository<TKey, TEntity extends IEntity<TKey>>
|
||||||
|
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
|
||||||
37
src/application/collections/collection.yaml.d.ts
vendored
37
src/application/collections/collection.yaml.d.ts
vendored
@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
export type DocumentationData = ReadonlyArray<string> | string;
|
export type DocumentationData = ReadonlyArray<string> | string | undefined;
|
||||||
|
|
||||||
export interface DocumentableData {
|
export interface DocumentableData {
|
||||||
readonly docs?: DocumentationData;
|
readonly docs?: DocumentationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstructionHolder {
|
export interface CodeInstruction {
|
||||||
readonly name: string;
|
readonly code: string;
|
||||||
|
|
||||||
readonly code?: string;
|
|
||||||
readonly revertCode?: string;
|
readonly revertCode?: string;
|
||||||
|
|
||||||
readonly call?: FunctionCallsData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CallInstruction {
|
||||||
|
readonly call: FunctionCallsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstructionHolder = CodeInstruction | CallInstruction;
|
||||||
|
|
||||||
export interface ParameterDefinitionData {
|
export interface ParameterDefinitionData {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly optional?: boolean;
|
readonly optional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionData extends InstructionHolder {
|
export type FunctionDefinition = {
|
||||||
|
readonly name: string;
|
||||||
readonly parameters?: readonly ParameterDefinitionData[];
|
readonly parameters?: readonly ParameterDefinitionData[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallFunctionData = FunctionDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type FunctionData = CodeFunctionData | CallFunctionData;
|
||||||
|
|
||||||
export interface FunctionCallParametersData {
|
export interface FunctionCallParametersData {
|
||||||
readonly [index: string]: string;
|
readonly [index: string]: string;
|
||||||
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
|
|||||||
|
|
||||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
export type ScriptDefinition = DocumentableData & {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly recommend?: string;
|
readonly recommend?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeScriptData = ScriptDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallScriptData = ScriptDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type ScriptData = CodeScriptData | CallScriptData;
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
readonly language: string;
|
readonly language: string;
|
||||||
|
|||||||
@@ -707,15 +707,9 @@ actions:
|
|||||||
-
|
-
|
||||||
category: Clear Firefox history
|
category: Clear Firefox history
|
||||||
docs: |-
|
docs: |-
|
||||||
Mozilla Firefox, or simply Firefox, is a free and open-source web browser developed by the Mozilla Foundation and
|
This category encompasses a series of scripts aimed at helping users manage and delete their browsing history and related data in Mozilla Firefox.
|
||||||
its subsidiary the Mozilla Corporation [1].
|
|
||||||
|
|
||||||
Firefox stores user-related data in user profiles [2].
|
The scripts are designed to target different aspects of user data stored by Firefox, providing users options for maintaining privacy and freeing up disk space.
|
||||||
|
|
||||||
See also [the Firefox homepage](https://web.archive.org/web/20221029214632/https://www.mozilla.org/en-US/firefox/).
|
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145113/https://en.wikipedia.org/wiki/Firefox "Firefox | Wikipedia | en.wikipedia.org"
|
|
||||||
[2]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Clear Firefox cache
|
name: Clear Firefox cache
|
||||||
@@ -755,9 +749,13 @@ actions:
|
|||||||
# Snap installation
|
# Snap installation
|
||||||
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
rm -rfv ~/snap/firefox/common/.mozilla/firefox/Crash\ Reports/*
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: crashes/
|
pathGlob: crashes/*
|
||||||
|
-
|
||||||
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
|
parameters:
|
||||||
|
pathGlob: crashes/events/*
|
||||||
-
|
-
|
||||||
name: Clear Firefox cookies
|
name: Clear Firefox cookies
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -765,41 +763,37 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
[1]: https://web.archive.org/web/20221029140816/https://kb.mozillazine.org/Cookies.sqlite "Cookies.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: cookies.sqlite
|
pathGlob: cookies.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
|
name: Clear Firefox browsing history (URLs, downloads, bookmarks, visits, etc.)
|
||||||
|
# This script (name, documentation and code) is same in Linux and Windows collections.
|
||||||
|
# Changes should be done at both places.
|
||||||
|
# Marked: refactor-with-partials
|
||||||
docs: |-
|
docs: |-
|
||||||
The file "places.sqlite" stores the annotations, bookmarks, favorite icons, input history, keywords, and browsing history (a record of visited pages) [1].
|
This script targets the Firefox browsing history, including URLs, downloads, bookmarks, and site visits, by deleting specific database entries.
|
||||||
The tables include [1]:
|
|
||||||
- `moz_anno_attributes`: Annotation Attributes
|
|
||||||
- `moz_annos`: Annotations
|
|
||||||
- `moz_bookmarks`: Bookmarks
|
|
||||||
- `moz_bookmarks_roots`: Bookmark roots i.e. places, menu, toolbar, tags, unfiled
|
|
||||||
- `moz_favicons`: Favorite icons - including URL of icon
|
|
||||||
- `moz_historyvisits`: A history of the number of times a site has been visited
|
|
||||||
- `moz_inputhistory`: A history of URLs typed by the user
|
|
||||||
- `moz_items_annos`: Item annotations
|
|
||||||
- `moz_keywords`: Keywords
|
|
||||||
- `moz_places`: Places/Sites visited - referenced by `moz_historyvisits`
|
|
||||||
URL data is stored in the `moz_places` table. However, this table is connected to `moz_annos`, `moz_bookmarks`, and `moz_inputhistory` and `moz_historyvisits`.
|
|
||||||
As these entries are connected to each other, we'll delete all of them at the same time [2].
|
|
||||||
|
|
||||||
**Bookmarks**:
|
Firefox stores various user data in a file named `places.sqlite`. This file includes:
|
||||||
Firefox bookmarks are stored in tables such as `moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots` [3].
|
|
||||||
There are also not very well documented tables, such as `moz_bookmarks_deleted` [4].
|
|
||||||
|
|
||||||
**Downloads:**
|
- Annotations, bookmarks, and favorite icons (`moz_anno_attributes`, `moz_annos`, `moz_favicons`) [1]
|
||||||
Firefox downloads are stored in the 'places.sqlite' database, within the 'moz_annos' table [5].
|
- Browsing history, a record of pages visited (`moz_places`, `moz_historyvisits`) [1]
|
||||||
The entries in `moz_annos` are linked to `moz_places` that store the actual history entry (`moz_places.id = moz_annos.place_id`) [6].
|
- Keywords and typed URLs (`moz_keywords`, `moz_inputhistory`) [1]
|
||||||
Associated URL information is stored within the 'moz_places' table [5].
|
- Item annotations (`moz_items_annos`) [1]
|
||||||
Downloads have been historically stored in `downloads.rdf` for Firefox 2.x and below [7].
|
- Bookmark roots such as places, menu, toolbar, tags, unfiled (`moz_bookmarks_roots`) [1]
|
||||||
Starting with Firefox 3.x they're stored in `downloads.sqlite` [7].
|
|
||||||
|
|
||||||
**Favicons:**
|
The `moz_places` table holds URL data, connecting to various other tables like `moz_annos`, `moz_bookmarks`, `moz_inputhistory`, and `moz_historyvisits` [2].
|
||||||
Firefox favicons are stored in the `favicons.sqlite` database, within the `moz_icons` table [5].
|
Due to these connections, the script removes entries from all relevant tables simultaneously to maintain database integrity.
|
||||||
Older versions of Firefox stored Favicons in the 'places.sqlite' database, within the `moz_favicons` table [5].
|
|
||||||
|
**Bookmarks**: Stored across several tables (`moz_bookmarks`, `moz_bookmarks_folders`, `moz_bookmarks_roots`) [3], with additional undocumented tables like `moz_bookmarks_deleted` [4].
|
||||||
|
|
||||||
|
**Downloads**: Stored in the 'places.sqlite' database, within the 'moz_annos' table [5]. The entries in `moz_annos` are linked to `moz_places` that store the actual history entry
|
||||||
|
(`moz_places.id = moz_annos.place_id`) [6]. Associated URL information is stored within the 'moz_places' table [5]. Downloads have been historically stored in `downloads.rdf` for Firefox 2.x
|
||||||
|
and below [7], and `downloads.sqlite` later on [7].
|
||||||
|
|
||||||
|
**Favicons**: Older Firefox versions stored favicons in `places.sqlite` within the `moz_favicons` table [5], while newer versions use `favicons.sqlite` and the `moz_icons` table [5].
|
||||||
|
|
||||||
|
By executing this script, users can ensure their Firefox browsing history, bookmarks, and downloads are thoroughly removed, contributing to a cleaner and more private browsing experience.
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
[1]: https://web.archive.org/web/20221029141626/https://kb.mozillazine.org/Places.sqlite "Places.sqlite - MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
|
[2]: https://web.archive.org/web/20221030160803/https://wiki.mozilla.org/images/0/08/Places.sqlite.schema.pdf "Places.sqlite.schema.pdf | Mozilla Wiki"
|
||||||
@@ -810,21 +804,21 @@ actions:
|
|||||||
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
|
[7]: https://web.archive.org/web/20221029145712/https://kb.mozillazine.org/Downloads.rdf "Downloads.rdf | MozillaZine Knowledge Base | kb.mozillazine.org"
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: downloads.rdf
|
pathGlob: downloads.rdf
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: downloads.sqlite
|
pathGlob: downloads.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: places.sqlite
|
pathGlob: places.sqlite
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: favicons.sqlite
|
pathGlob: favicons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox logins
|
name: Clear Firefox logins
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -837,17 +831,17 @@ actions:
|
|||||||
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
|
[2]: https://web.archive.org/web/20221029145757/https://bugzilla.mozilla.org/show_bug.cgi?id=1593467 "1593467 - Automatically restore from logins-backup.json when logins.json is missing or corrupt | Bugzilla | mozilla.org | bugzilla.mozilla.org"
|
||||||
call:
|
call:
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: logins.json
|
pathGlob: logins.json
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: logins-backup.json
|
pathGlob: logins-backup.json
|
||||||
-
|
-
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: signons.sqlite
|
pathGlob: signons.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox autocomplete history
|
name: Clear Firefox autocomplete history
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -856,9 +850,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: formhistory.sqlite
|
pathGlob: formhistory.sqlite
|
||||||
-
|
-
|
||||||
name: Clear Firefox "Multi-Account Containers" data
|
name: Clear Firefox "Multi-Account Containers" data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -866,9 +860,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: containers.json
|
pathGlob: containers.json
|
||||||
-
|
-
|
||||||
name: Clear Firefox open tabs and windows data
|
name: Clear Firefox open tabs and windows data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -878,9 +872,9 @@ actions:
|
|||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221029145152/https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data "Profiles - Where Firefox stores your bookmarks, passwords and other user data | Firefox Help | support.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: DeleteFromFirefoxProfiles
|
function: DeleteFilesFromFirefoxProfiles
|
||||||
parameters:
|
parameters:
|
||||||
path: sessionstore.jsonlz4
|
pathGlob: sessionstore.jsonlz4
|
||||||
-
|
-
|
||||||
category: Clear system and kernel usage data
|
category: Clear system and kernel usage data
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -2911,7 +2905,8 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.level
|
prefName: toolkit.telemetry.log.level
|
||||||
jsonValue: 'Fatal'
|
jsonValue: >-
|
||||||
|
"Fatal"
|
||||||
-
|
-
|
||||||
name: Disable Firefox telemetry log output
|
name: Disable Firefox telemetry log output
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -2924,7 +2919,8 @@ actions:
|
|||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
parameters:
|
parameters:
|
||||||
prefName: toolkit.telemetry.log.dump
|
prefName: toolkit.telemetry.log.dump
|
||||||
jsonValue: 'Fatal'
|
jsonValue: >-
|
||||||
|
"Fatal"
|
||||||
-
|
-
|
||||||
name: Clear Firefox telemetry user ID
|
name: Clear Firefox telemetry user ID
|
||||||
recommend: standard
|
recommend: standard
|
||||||
@@ -3491,16 +3487,66 @@ functions:
|
|||||||
>&2 echo "Failed, $service does not exist."
|
>&2 echo "Failed, $service does not exist."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: DeleteFromFirefoxProfiles
|
name: Comment
|
||||||
|
# 💡 Purpose:
|
||||||
|
# Adds a comment in the executed code for better readability and debugging.
|
||||||
|
# This function does not affect the execution flow but helps in understanding the purpose of subsequent code.
|
||||||
|
parameters:
|
||||||
|
- name: codeComment
|
||||||
|
optional: true
|
||||||
|
- name: revertCodeComment
|
||||||
|
optional: true
|
||||||
|
call:
|
||||||
|
function: RunInlineCode
|
||||||
|
parameters:
|
||||||
|
code: '{{ with $codeComment }}# {{ . }}{{ end }}'
|
||||||
|
revertCode: '{{ with $revertCodeComment }}# {{ . }}{{ end }}'
|
||||||
|
-
|
||||||
|
name: DeleteFiles
|
||||||
|
parameters:
|
||||||
|
- name: fileGlob
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: Comment
|
||||||
|
parameters:
|
||||||
|
codeComment: >-
|
||||||
|
Delete files matching pattern: "{{ $fileGlob }}"
|
||||||
|
-
|
||||||
|
function: RunPython3Code
|
||||||
parameters:
|
parameters:
|
||||||
- name: path # file or folder in profile file
|
|
||||||
code: |-
|
code: |-
|
||||||
# {{ $path }}: Global installation
|
import glob
|
||||||
rm -rfv ~/.mozilla/firefox/*/{{ $path }}
|
import os
|
||||||
# {{ $path }}: Flatpak installation
|
path = '{{ $fileGlob }}'
|
||||||
rm -rfv ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $path }}
|
expanded_path = os.path.expandvars(os.path.expanduser(path))
|
||||||
# {{ $path }}: Snap installation
|
print(f'Deleting files matching pattern: {expanded_path}')
|
||||||
rm -rfv ~/snap/firefox/common/.mozilla/firefox/*/{{ $path }}
|
paths = glob.glob(expanded_path)
|
||||||
|
if not paths:
|
||||||
|
print('Skipping, no paths found.')
|
||||||
|
for path in paths:
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
print(f'Skipping folder: "{path}".')
|
||||||
|
continue
|
||||||
|
os.remove(path)
|
||||||
|
print(f'Successfully delete file: "{path}".')
|
||||||
|
print(f'Successfully deleted {len(paths)} file(s).')
|
||||||
|
-
|
||||||
|
name: DeleteFilesFromFirefoxProfiles
|
||||||
|
parameters:
|
||||||
|
- name: pathGlob # file or folder in profile file
|
||||||
|
call:
|
||||||
|
- # Global installation
|
||||||
|
function: DeleteFiles
|
||||||
|
parameters:
|
||||||
|
fileGlob: ~/.mozilla/firefox/*/{{ $pathGlob }}
|
||||||
|
- # Flatpak installation
|
||||||
|
function: DeleteFiles
|
||||||
|
parameters:
|
||||||
|
fileGlob: ~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/{{ $pathGlob }}
|
||||||
|
- # Snap installation
|
||||||
|
function: DeleteFiles
|
||||||
|
parameters:
|
||||||
|
fileGlob: ~/snap/firefox/common/.mozilla/firefox/*/{{ $pathGlob }}
|
||||||
-
|
-
|
||||||
name: CleanTableFromFirefoxProfileDatabase
|
name: CleanTableFromFirefoxProfileDatabase
|
||||||
parameters:
|
parameters:
|
||||||
@@ -3693,7 +3739,7 @@ functions:
|
|||||||
- name: prefName
|
- name: prefName
|
||||||
- name: jsonValue
|
- name: jsonValue
|
||||||
docs: |-
|
docs: |-
|
||||||
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
|
This script modifies the `user.js` file in Firefox profiles to set specific preferences.
|
||||||
|
|
||||||
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
||||||
|
|
||||||
@@ -3701,12 +3747,18 @@ functions:
|
|||||||
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
||||||
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
||||||
|
|
||||||
While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
|
While the `user.js` file is optional [2], if it's present, the Firefox will prioritize its settings over
|
||||||
those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
|
those in `prefs.js` upon startup [1] [2]. It's recommended not to directly edit `prefs.js` to avoid profile corruption [2].
|
||||||
`prefs.js` directly [2].
|
|
||||||
|
When `user.js` is modified or deleted, corresponding changes in `prefs.js` are necessary for reversion, as Firefox
|
||||||
|
doesn't automatically revert these changes [3].
|
||||||
|
|
||||||
|
This script safely modifies `user.js` and ensures changes are reflected in `prefs.js` during reversion, addressing
|
||||||
|
issues with preference persistence [3].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
||||||
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
||||||
|
[3]: https://github.com/undergroundwires/privacy.sexy/issues/282 "[BUG]: Reverting Firefox settings do not work on Linux · Issue #282 · undergroundwires/privacy.sexy | github.com"
|
||||||
code: |-
|
code: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
@@ -3746,12 +3798,16 @@ functions:
|
|||||||
if [ "$total_profiles_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo 'No profile folders are found, no changes are made.'
|
echo 'No profile folders are found, no changes are made.'
|
||||||
else
|
else
|
||||||
echo "Preferences verified in $total_profiles_found profiles."
|
echo "Successfully verified preferences in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
revertCode: |-
|
revertCode: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
echo "Reverting preference: \"$pref_name\" to its default."
|
echo "Reverting preference: \"$pref_name\" to its default."
|
||||||
|
if command -v 'ps' &> /dev/null && ps aux | grep -i "[f]irefox" > /dev/null; then
|
||||||
|
>&2 echo -e "\e[33mWarning: Firefox is currently running. Please close Firefox before executing the revert script to ensure changes are applied effectively.\e[0m"
|
||||||
|
fi
|
||||||
|
declare -a files_to_modify=('prefs.js' 'user.js')
|
||||||
declare -a profile_paths=(
|
declare -a profile_paths=(
|
||||||
~/.mozilla/firefox/*/
|
~/.mozilla/firefox/*/
|
||||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||||
@@ -3759,31 +3815,39 @@ functions:
|
|||||||
)
|
)
|
||||||
declare -i total_profiles_found=0
|
declare -i total_profiles_found=0
|
||||||
for profile_dir in "${profile_paths[@]}"; do
|
for profile_dir in "${profile_paths[@]}"; do
|
||||||
user_js_file="${profile_dir}user.js"
|
if [ ! -d "$profile_dir" ]; then
|
||||||
if [ ! -f "$user_js_file" ]; then
|
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
|
||||||
|
continue # Not a profile folder
|
||||||
|
fi
|
||||||
((total_profiles_found++))
|
((total_profiles_found++))
|
||||||
echo "$user_js_file:"
|
for file_to_modify in "${files_to_modify[@]}"; do
|
||||||
|
config_file_path="${profile_dir}${file_to_modify}"
|
||||||
|
if [ ! -f "$config_file_path" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "$config_file_path:"
|
||||||
pref_start="user_pref(\"$pref_name\","
|
pref_start="user_pref(\"$pref_name\","
|
||||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
if ! grep --quiet "^$pref_start" "${config_file_path}"; then
|
||||||
echo $'\t''Skipping, preference was not configured before.'
|
echo $'\t''Skipping, preference was not configured before.'
|
||||||
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
|
elif grep --quiet "^$pref_line$" "${config_file_path}"; then
|
||||||
sed --in-place "/^$pref_line/d" "$user_js_file"
|
sed --in-place "/^$pref_line/d" "$config_file_path"
|
||||||
echo $'\t''Successfully reverted preference to default.'
|
echo $'\t''Successfully reverted preference to default.'
|
||||||
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
|
if ! grep --quiet '[^[:space:]]' "$config_file_path"; then
|
||||||
rm "$user_js_file"
|
rm "$config_file_path"
|
||||||
echo $'\t''Removed user.js file as it became empty.'
|
echo $'\t'"Removed the file as it became empty."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
done
|
||||||
if [ "$total_profiles_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo 'No reversion was necessary.'
|
echo 'No reversion was necessary.'
|
||||||
else
|
else
|
||||||
echo "Preferences verified in $total_profiles_found profiles."
|
echo "Successfully verified preferences in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: RenameFile
|
name: RenameFile
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ export class Application implements IApplication {
|
|||||||
public info: IProjectInformation,
|
public info: IProjectInformation,
|
||||||
public collections: readonly ICategoryCollection[],
|
public collections: readonly ICategoryCollection[],
|
||||||
) {
|
) {
|
||||||
validateInformation(info);
|
|
||||||
validateCollections(collections);
|
validateCollections(collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,19 +15,17 @@ export class Application implements IApplication {
|
|||||||
return this.collections.map((collection) => collection.os);
|
return this.collections.map((collection) => collection.os);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
|
||||||
return this.collections.find((collection) => collection.os === operatingSystem);
|
const collection = this.collections.find((c) => c.os === operatingSystem);
|
||||||
|
if (!collection) {
|
||||||
|
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
|
||||||
}
|
}
|
||||||
}
|
return collection;
|
||||||
|
|
||||||
function validateInformation(info: IProjectInformation) {
|
|
||||||
if (!info) {
|
|
||||||
throw new Error('missing project information');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||||
if (!collections || !collections.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.filter((c) => !c).length > 0) {
|
if (collections.filter((c) => !c).length > 0) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly docs: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>,
|
public readonly scripts: ReadonlyArray<IScript>,
|
||||||
) {
|
) {
|
||||||
super(id);
|
super(id);
|
||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
@@ -39,10 +39,7 @@ function validateCategory(category: ICategory) {
|
|||||||
if (!category.name) {
|
if (!category.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (
|
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
||||||
(!category.subCategories || category.subCategories.length === 0)
|
|
||||||
&& (!category.scripts || category.scripts.length === 0)
|
|
||||||
) {
|
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
public readonly actions: ReadonlyArray<ICategory>,
|
public readonly actions: ReadonlyArray<ICategory>,
|
||||||
public readonly scripting: IScriptingDefinition,
|
public readonly scripting: IScriptingDefinition,
|
||||||
) {
|
) {
|
||||||
if (!scripting) {
|
|
||||||
throw new Error('missing scripting definition');
|
|
||||||
}
|
|
||||||
this.queryable = makeQueryable(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
assertInRange(os, OperatingSystem);
|
assertInRange(os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
@@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public getCategory(categoryId: number): ICategory {
|
||||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||||
|
if (!category) {
|
||||||
|
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||||
|
}
|
||||||
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
assertInRange(level, RecommendationLevel);
|
assertInRange(level, RecommendationLevel);
|
||||||
return this.queryable.scriptsByLevel.get(level);
|
const scripts = this.queryable.scriptsByLevel.get(level);
|
||||||
|
return scripts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
public getScript(scriptId: string): IScript {
|
||||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||||
|
if (!script) {
|
||||||
|
throw new Error(`missing script: ${scriptId}`);
|
||||||
|
}
|
||||||
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
public getAllScripts(): IScript[] {
|
||||||
@@ -78,13 +84,13 @@ function ensureValid(application: IQueryableCollection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||||
if (!allCategories || allCategories.length === 0) {
|
if (!allCategories.length) {
|
||||||
throw new Error('must consist of at least one category');
|
throw new Error('must consist of at least one category');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||||
if (!allScripts || allScripts.length === 0) {
|
if (!allScripts.length) {
|
||||||
throw new Error('must consist of at least one script');
|
throw new Error('must consist of at least one script');
|
||||||
}
|
}
|
||||||
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export interface IApplication {
|
|||||||
readonly collections: readonly ICategoryCollection[];
|
readonly collections: readonly ICategoryCollection[];
|
||||||
|
|
||||||
getSupportedOsList(): OperatingSystem[];
|
getSupportedOsList(): OperatingSystem[];
|
||||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
getCollection(operatingSystem: OperatingSystem): ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
|
|||||||
export interface ICategory extends IEntity<number>, IDocumentable {
|
export interface ICategory extends IEntity<number>, IDocumentable {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts: ReadonlyArray<IScript>;
|
||||||
includes(script: IScript): boolean;
|
includes(script: IScript): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface ICategoryCollection {
|
|||||||
readonly actions: ReadonlyArray<ICategory>;
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
getCategory(categoryId: number): ICategory;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
getScript(scriptId: string): IScript;
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
getAllCategories(): ReadonlyArray<ICategory>;
|
getAllCategories(): ReadonlyArray<ICategory>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IScriptCode {
|
export interface IScriptCode {
|
||||||
readonly execute: string;
|
readonly execute: string;
|
||||||
readonly revert: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('name is undefined');
|
throw new Error('name is undefined');
|
||||||
}
|
}
|
||||||
if (!version) {
|
|
||||||
throw new Error('undefined version');
|
|
||||||
}
|
|
||||||
if (!slogan) {
|
if (!slogan) {
|
||||||
throw new Error('undefined slogan');
|
throw new Error('undefined slogan');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
public readonly level?: RecommendationLevel,
|
public readonly level?: RecommendationLevel,
|
||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
|
||||||
throw new Error('missing code');
|
|
||||||
}
|
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
export class ScriptCode implements IScriptCode {
|
export class ScriptCode implements IScriptCode {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string | undefined,
|
||||||
) {
|
) {
|
||||||
validateCode(execute);
|
validateCode(execute);
|
||||||
validateRevertCode(revert, execute);
|
validateRevertCode(revert, execute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string) {
|
function validateRevertCode(revertCode: string | undefined, execute: string) {
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user