Compare commits

..

1 Commits

Author SHA1 Message Date
undergroundwires
f9a54c7e68 Fix Colima builds failing 2024-05-20 17:32:21 +02:00
469 changed files with 11118 additions and 30828 deletions

View File

@@ -1,15 +0,0 @@
inputs:
name:
required: true
path:
required: true
runs:
using: composite
steps:
-
name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -86,9 +86,21 @@ jobs:
name: Install Docker on macOS name: Install Docker on macOS
if: contains(matrix.os, 'macos') # macOS runner is missing Docker if: contains(matrix.os, 'macos') # macOS runner is missing Docker
run: |- run: |-
# Verify Intel-based macOS
arch=$(uname -m)
case "$arch" in
i386|x86_64)
echo "Supported architecture: $arch"
;;
*)
>&2 echo 'The macOS is not running on a supported Intel architecture. Virtualization is not supported.'
exit 1
;;
esac
# Install Docker # Install Docker
brew install docker brew install docker
# Docker on macOS misses daemon due to licensing, so install colima as runtime # Docker on macOS does not include the Docker daemon due to licensing issues.
# Install Colima to use as the Docker runtime.
brew install colima brew install colima
# Start the daemon # Start the daemon
colima start colima start

View File

@@ -10,8 +10,8 @@ jobs:
strategy: strategy:
matrix: matrix:
os: os:
- macos-latest # Apple silicon (ARM64) - macos-latest # Latest Apple silicon (ARM64)
- macos-13 # Intel-based (x86-64) - macos-12 # Latest Intel-based (x86-64)
- ubuntu-latest - ubuntu-latest
- windows-latest - windows-latest
fail-fast: false # Allows to see results from other combinations fail-fast: false # Allows to see results from other combinations
@@ -70,7 +70,7 @@ jobs:
- -
name: Upload screenshot name: Upload screenshot
if: always() # Run even if previous step fails if: always() # Run even if previous step fails
uses: ./.github/actions/upload-artifact uses: actions/upload-artifact@v3
with: with:
name: screenshot-${{ matrix.os }} name: screenshot-${{ matrix.os }}
path: screenshot.png path: screenshot.png

View File

@@ -74,28 +74,3 @@ jobs:
- -
name: Analyzing the code with pylint name: Analyzing the code with pylint
run: npm run lint:pylint run: npm run lint:pylint
validate-collection-files:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
-
name: Install dependencies
run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
-
name: Validate
run: python3 ./scripts/validate-collections-yaml

View File

@@ -51,14 +51,14 @@ jobs:
- -
name: Upload screenshots name: Upload screenshots
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
uses: ./.github/actions/upload-artifact uses: actions/upload-artifact@v3
with: with:
name: e2e-screenshots-${{ matrix.os }} name: e2e-screenshots-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }} path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
- -
name: Upload videos name: Upload videos
if: always() # Run even if previous steps fail because test run video is always captured if: always() # Run even if previous steps fail because test run video is always captured
uses: ./.github/actions/upload-artifact uses: actions/upload-artifact@v3
with: with:
name: e2e-videos-${{ matrix.os }} name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }} path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

4
.gitignore vendored
View File

@@ -14,7 +14,3 @@ node_modules
# macOS # macOS
.DS_Store .DS_Store
# Python
__pycache__
.venv

View File

@@ -5,10 +5,8 @@
"wengerk.highlight-bad-chars", // Highlights bad chars. "wengerk.highlight-bad-chars", // Highlights bad chars.
"wayou.vscode-todo-highlight", // Highlights TODO. "wayou.vscode-todo-highlight", // Highlights TODO.
"wix.vscode-import-cost", // Shows in KB how much a require include in code. "wix.vscode-import-cost", // Shows in KB how much a require include in code.
// Markdown // Documentation
"davidanson.vscode-markdownlint", // Lints markdown. "davidanson.vscode-markdownlint", // Lints markdown.
// YAML
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
// TypeScript / JavaScript // TypeScript / JavaScript
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript. "dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports. "pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.

View File

@@ -1,54 +1,5 @@
# Changelog # Changelog
## 0.13.5 (2024-06-26)
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8)
* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277)
* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6)
* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3)
* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e)
* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99)
* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd)
* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8)
* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e)
* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536)
* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb)
* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e)
* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8)
* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac)
* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7)
* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2)
* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2)
* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde)
* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59)
* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4)
* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5)
## 0.13.4 (2024-05-27)
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
## 0.13.3 (2024-05-11) ## 0.13.3 (2024-05-11)
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd) * win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)

View File

@@ -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.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.AppImage). For more options, see [here](#additional-install-options). - 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-Setup-0.13.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.AppImage). For more options, see [here](#additional-install-options).
See also: See also:
@@ -186,7 +186,3 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
Security is a top priority at privacy.sexy. Security is a top priority at privacy.sexy.
An extensive commitment to security verification ensures this priority. An extensive commitment to security verification ensures this priority.
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md). For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
## Supporters
[![Supporters appreciation banner showing the supporters](https://undergroundwires.dev/img/supporters.jpg)](https://undergroundwires.dev/supporters)

View File

@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
The steps to extend the templating syntax: The steps to extend the templating syntax:
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more. 1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts). 2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).

View File

@@ -1,11 +1,11 @@
# Collection files # Collection files
privacy.sexy is a data-driven application that reads YAML files. privacy.sexy is a data-driven application that reads YAML files.
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations. This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model.
Related documentation: Related documentation:
- 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation. - 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types.
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices. - 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
## Objects ## Objects
@@ -28,22 +28,11 @@ Related documentation:
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)** - `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
- Sets the scripting language for all inline code used within the collection. - Sets the scripting language for all inline code used within the collection.
### Executables ### `Category`
They represent independently executable actions with documentation and reversibility.
An Executable is a logical entity that can
- execute once compiled,
- include a `docs` property for documentation.
It's either [Category](#category) or a [Script](#script).
#### `Category`
Represents a logical group of scripts and subcategories. Represents a logical group of scripts and subcategories.
##### `Category` syntax #### `Category` syntax
- `category:` *`string`* **(required)** - `category:` *`string`* **(required)**
- Name of the category. - Name of the category.
@@ -54,7 +43,7 @@ Represents a logical group of scripts and subcategories.
- `docs`: *`string`* | `[`*`string`*`, ... ]` - `docs`: *`string`* | `[`*`string`*`, ... ]`
- Markdown-formatted documentation related to the category. - Markdown-formatted documentation related to the category.
#### `Script` ### `Script`
Represents an individual tweak. Represents an individual tweak.
@@ -69,7 +58,7 @@ Types (like [functions](#function)):
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md). 📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
##### `Script` syntax #### `Script` syntax
- `name`: *`string`* **(required)** - `name`: *`string`* **(required)**
- Script name. - Script name.

View File

@@ -80,10 +80,8 @@ See [ci-cd.md](./ci-cd.md) for more information.
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js): - [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features. - Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies. - For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
- [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): - [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment. - Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
- Validates the syntax and structure of collection YAML files.
#### Automation scripts #### Automation scripts

5416
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.5", "version": "0.13.3",
"private": true, "private": true,
"slogan": "Privacy is sexy", "slogan": "Privacy is sexy",
"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.",
@@ -34,54 +34,54 @@
"postuninstall": "electron-builder install-app-deps" "postuninstall": "electron-builder install-app-deps"
}, },
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.1.1", "@floating-ui/vue": "^1.0.6",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.35.3", "ace-builds": "^1.33.0",
"electron-log": "^5.1.6", "electron-log": "^5.1.2",
"electron-progressbar": "^2.2.1", "electron-progressbar": "^2.2.1",
"electron-updater": "^6.2.1", "electron-updater": "^6.1.9",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"vue": "^3.4.32" "vue": "^3.4.27"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@rushstack/eslint-patch": "^1.10.3", "@rushstack/eslint-patch": "^1.10.2",
"@types/ace": "^0.0.52", "@types/ace": "^0.0.52",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/markdown-it": "^14.1.1", "@types/markdown-it": "^14.0.1",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0", "@typescript-eslint/parser": "6.21.0",
"@vitejs/plugin-legacy": "^5.4.1", "@vitejs/plugin-legacy": "^5.3.2",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0", "@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
"@vue/eslint-config-typescript": "12.0.0", "@vue/eslint-config-typescript": "12.0.0",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.13.1", "cypress": "^13.7.3",
"electron": "^31.2.1", "electron": "^29.3.0",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.3.0", "electron-vite": "^2.1.0",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-plugin-cypress": "^3.3.0", "eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-vue": "^9.27.0", "eslint-plugin-vue": "^9.25.0",
"eslint-plugin-vuejs-accessibility": "^2.4.0", "eslint-plugin-vuejs-accessibility": "^2.2.1",
"jsdom": "^24.1.0", "jsdom": "^24.0.0",
"markdownlint-cli": "^0.41.0", "markdownlint-cli": "^0.39.0",
"postcss": "^8.4.39", "postcss": "^8.4.38",
"remark-cli": "^12.0.1", "remark-cli": "^12.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^6.0.0", "remark-preset-lint-consistent": "^6.0.0",
"remark-validate-links": "^13.0.1", "remark-validate-links": "^13.0.1",
"sass": "^1.77.8", "sass": "^1.75.0",
"start-server-and-test": "^2.0.4", "start-server-and-test": "^2.0.3",
"terser": "^5.31.3", "terser": "^5.30.3",
"tslib": "^2.6.3", "tslib": "^2.6.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.3.4", "vite": "^5.2.8",
"vitest": "^2.0.3", "vitest": "^1.5.0",
"vue-tsc": "^2.0.26", "vue-tsc": "^2.0.13",
"yaml-lint": "^1.7.0" "yaml-lint": "^1.7.0"
}, },
"//devDependencies": { "//devDependencies": {

View File

@@ -58,10 +58,6 @@ def add_or_update_settings() -> None:
# Details: # pylint: disable-next=line-too-long # Details: # pylint: disable-next=line-too-long
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274 # - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
# Disable telemetry
configure_setting_key('redhat.telemetry.enabled', False)
configure_setting_key('gitlens.telemetry.enabled', False)
def configure_setting_key(configuration_key: str, desired_value: Any) -> None: def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
try: try:
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file: with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:

View File

@@ -1,51 +0,0 @@
# validate-collections-yaml
This script validates YAML collection files against a predefined schema to ensure their integrity.
## Prerequisites
- Python 3.x installed on your system.
## Running in a Virtual Environment (Recommended)
Using a virtual environment isolates dependencies and prevents conflicts.
1. **Create a virtual environment:**
```bash
python3 -m venv ./scripts/validate-collections-yaml/.venv
```
2. **Activate the virtual environment:**
```bash
source ./scripts/validate-collections-yaml/.venv/bin/activate
```
3. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
4. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```
## Running Globally
Running the script globally is less recommended due to potential dependency conflicts.
1. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
2. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```

View File

@@ -1,62 +0,0 @@
"""
Description:
This script validates collection YAML files against the expected schema.
Usage:
python3 ./scripts/validate-collections-yaml
Notes:
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
"""
# pylint: disable=missing-function-docstring
from os import path
import sys
from glob import glob
from typing import List
from jsonschema import exceptions, validate # pylint: disable=import-error
import yaml # pylint: disable=import-error
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
def main() -> None:
schema_yaml = read_file(SCHEMA_FILE_PATH)
schema_json = convert_yaml_to_json(schema_yaml)
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
print(f'Found {len(collection_file_paths)} YAML files to validate.')
total_invalid_files = 0
for collection_file_path in collection_file_paths:
file_name = path.basename(collection_file_path)
print(f'Validating {file_name}...')
collection_yaml = read_file(collection_file_path)
collection_json = convert_yaml_to_json(collection_yaml)
try:
validate(instance=collection_json, schema=schema_json)
print(f'Success: {file_name} is valid.')
except exceptions.ValidationError as err:
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
print(str(err), file=sys.stderr)
total_invalid_files += 1
if total_invalid_files > 0:
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
sys.exit(1)
else:
print('Validation complete. All files are valid.')
sys.exit(0)
def read_file(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
def find_collection_files(glob_pattern: str) -> List[str]:
files = glob(glob_pattern)
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
return filtered_files
def convert_yaml_to_json(yaml_content: str) -> dict:
return yaml.safe_load(yaml_content)
if __name__ == '__main__':
main()

View File

@@ -1,6 +0,0 @@
attrs==23.2.0
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
PyYAML==6.0.1
referencing==0.35.1
rpds-py==0.18.1

View File

@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
if (!match) { if (!match) {
die( die(
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``, `No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
`\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`, `\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
); );
} }
} }

View File

@@ -1,4 +1,4 @@
import { isFunction, type ConstructorArguments } from '@/TypeHelpers'; import { isFunction } from '@/TypeHelpers';
/* /*
Provides a unified and resilient way to extend errors across platforms. Provides a unified and resilient way to extend errors across platforms.
@@ -12,8 +12,8 @@ import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work > https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
*/ */
export abstract class CustomError extends Error { export abstract class CustomError extends Error {
constructor(...args: ConstructorArguments<typeof Error>) { constructor(message?: string, options?: ErrorOptions) {
super(...args); super(message, options);
fixPrototype(this, new.target.prototype); fixPrototype(this, new.target.prototype);
ensureStackTrace(this); ensureStackTrace(this);

View File

@@ -5,13 +5,13 @@ export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
= { [key in T]: TEnumValue }; = { [key in T]: TEnumValue };
export interface EnumParser<TEnum> { export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
} }
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
): EnumParser<TEnumValue> { ): IEnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };

View File

@@ -1,25 +0,0 @@
import { isArray } from '@/TypeHelpers';
export type OptionalString = string | undefined | null;
export function filterEmptyStrings(
texts: readonly OptionalString[],
isArrayType: typeof isArray = isArray,
): string[] {
if (!isArrayType(texts)) {
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
}
assertArrayItemsAreStringLike(texts);
return texts
.filter((title): title is string => Boolean(title));
}
function assertArrayItemsAreStringLike(
texts: readonly unknown[],
): asserts texts is readonly OptionalString[] {
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
if (invalidItems.length > 0) {
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
}
}

View File

@@ -1,29 +0,0 @@
import { isString } from '@/TypeHelpers';
import { splitTextIntoLines } from './SplitTextIntoLines';
export function indentText(
text: string,
indentLevel = 1,
utilities: TextIndentationUtilities = DefaultUtilities,
): string {
if (!utilities.isStringType(text)) {
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
}
if (indentLevel <= 0) {
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
}
const indentation = '\t'.repeat(indentLevel);
return utilities.splitIntoLines(text)
.map((line) => (line ? `${indentation}${line}` : line))
.join('\n');
}
interface TextIndentationUtilities {
readonly splitIntoLines: typeof splitTextIntoLines;
readonly isStringType: typeof isString;
}
const DefaultUtilities: TextIndentationUtilities = {
splitIntoLines: splitTextIntoLines,
isStringType: isString,
};

View File

@@ -1,11 +0,0 @@
import { isString } from '@/TypeHelpers';
export function splitTextIntoLines(
text: string,
isStringType = isString,
): string[] {
if (!isStringType(text)) {
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
}
return text.split(/\r\n|\r|\n/);
}

View File

@@ -1,6 +1,6 @@
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState'; import { CategoryCollectionState } from './State/CategoryCollectionState';

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';

View File

@@ -1,20 +1,18 @@
import type { Script } from '@/domain/Executables/Script/Script'; import type { IScript } from '@/domain/IScript';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { ICodeChangedEvent } from './ICodeChangedEvent'; import type { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string; public readonly code: string;
public readonly addedScripts: ReadonlyArray<Script>; public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<Script>; public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<Script>; public readonly changedScripts: ReadonlyArray<IScript>;
private readonly scripts: Map<Script, ICodePosition>; private readonly scripts: Map<IScript, ICodePosition>;
constructor( constructor(
code: string, code: string,
@@ -27,7 +25,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
this.addedScripts = selectIfNotExists(newScripts, oldScripts); this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts); this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts); this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<Script, ICodePosition>(); this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => { scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position); this.scripts.set(selection.script, position);
}); });
@@ -37,13 +35,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
return this.scripts.size === 0; return this.scripts.size === 0;
} }
public getScriptPositionInCode(script: Script): ICodePosition { public getScriptPositionInCode(script: IScript): ICodePosition {
return this.getPositionById(script.executableId); return this.getPositionById(script.id);
} }
private getPositionById(scriptId: ExecutableId): ICodePosition { private getPositionById(scriptId: string): ICodePosition {
const position = [...this.scripts.entries()] const position = [...this.scripts.entries()]
.filter(([s]) => s.executableId === scriptId) .filter(([s]) => s.id === scriptId)
.map(([, pos]) => pos) .map(([, pos]) => pos)
.at(0); .at(0);
if (!position) { if (!position) {
@@ -54,12 +52,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
} }
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) { function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = splitTextIntoLines(script).length; const totalLines = script.split(/\r\n|\r|\n/).length;
const missingPositions = positions.filter((position) => position.endLine > totalLines); const missingPositions = positions.filter((position) => position.endLine > totalLines);
if (missingPositions.length > 0) { if (missingPositions.length > 0) {
throw new Error( throw new Error(
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` `Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
+ ` (total code lines: ${totalLines}).`, + `(total code lines: ${totalLines}).`,
); );
} }
} }
@@ -67,7 +65,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>, newScripts: ReadonlyArray<SelectedScript>,
): ReadonlyArray<Script> { ): ReadonlyArray<IScript> {
return newScripts return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert)) && oldScript.revert !== newScript.revert))

View File

@@ -1,11 +1,11 @@
import type { Script } from '@/domain/Executables/Script/Script'; import type { IScript } from '@/domain/IScript';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent { export interface ICodeChangedEvent {
readonly code: string; readonly code: string;
readonly addedScripts: ReadonlyArray<Script>; readonly addedScripts: ReadonlyArray<IScript>;
readonly removedScripts: ReadonlyArray<Script>; readonly removedScripts: ReadonlyArray<IScript>;
readonly changedScripts: ReadonlyArray<Script>; readonly changedScripts: ReadonlyArray<IScript>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: Script): ICodePosition; getScriptPositionInCode(script: IScript): ICodePosition;
} }

View File

@@ -1,4 +1,3 @@
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { ICodeBuilder } from './ICodeBuilder'; import type { ICodeBuilder } from './ICodeBuilder';
const TotalFunctionSeparatorChars = 58; const TotalFunctionSeparatorChars = 58;
@@ -16,7 +15,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
this.lines.push(''); this.lines.push('');
return this; return this;
} }
const lines = splitTextIntoLines(code); const lines = code.match(/[^\r\n]+/g);
if (lines) { if (lines) {
this.lines.push(...lines); this.lines.push(...lines);
} }

View File

@@ -1,5 +1,5 @@
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { FilterChange } from './Event/FilterChange'; import { FilterChange } from './Event/FilterChange';
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
import type { FilterResult } from './Result/FilterResult'; import type { FilterResult } from './Result/FilterResult';

View File

@@ -1,11 +1,11 @@
import type { Script } from '@/domain/Executables/Script/Script'; import type { IScript } from '@/domain/IScript';
import type { Category } from '@/domain/Executables/Category/Category'; import type { ICategory } from '@/domain/ICategory';
import type { FilterResult } from './FilterResult'; import type { FilterResult } from './FilterResult';
export class AppliedFilterResult implements FilterResult { export class AppliedFilterResult implements FilterResult {
constructor( constructor(
public readonly scriptMatches: ReadonlyArray<Script>, public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<Category>, public readonly categoryMatches: ReadonlyArray<ICategory>,
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'); }

View File

@@ -1,9 +1,8 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { IScript, ICategory } from '@/domain/ICategory';
import type { Script } from '@/domain/Executables/Script/Script';
export interface FilterResult { export interface FilterResult {
readonly categoryMatches: ReadonlyArray<Category>; readonly categoryMatches: ReadonlyArray<ICategory>;
readonly scriptMatches: ReadonlyArray<Script>; readonly scriptMatches: ReadonlyArray<IScript>;
readonly query: string; readonly query: string;
hasAnyMatches(): boolean; hasAnyMatches(): boolean;
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
export interface FilterStrategy { export interface FilterStrategy {

View File

@@ -1,8 +1,7 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { ICategory, IScript } from '@/domain/ICategory';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { IScriptCode } from '@/domain/IScriptCode';
import type { Documentable } from '@/domain/Executables/Documentable'; import type { IDocumentable } from '@/domain/IDocumentable';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script';
import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import { AppliedFilterResult } from '../Result/AppliedFilterResult';
import type { FilterStrategy } from './FilterStrategy'; import type { FilterStrategy } from './FilterStrategy';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
@@ -25,7 +24,7 @@ export class LinearFilterStrategy implements FilterStrategy {
} }
function matchesCategory( function matchesCategory(
category: Category, category: ICategory,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -35,7 +34,7 @@ function matchesCategory(
} }
function matchesScript( function matchesScript(
script: Script, script: IScript,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -59,7 +58,7 @@ function matchName(
} }
function matchCode( function matchCode(
code: ScriptCode, code: IScriptCode,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
if (code.execute.toLowerCase().includes(filterLowercase)) { if (code.execute.toLowerCase().includes(filterLowercase)) {
@@ -72,7 +71,7 @@ function matchCode(
} }
function matchDocumentation( function matchDocumentation(
documentable: Documentable, documentable: IDocumentable,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return documentable.docs.some( return documentable.docs.some(

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { IApplicationCode } from './Code/IApplicationCode'; import type { IApplicationCode } from './Code/IApplicationCode';
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';

View File

@@ -1,9 +1,9 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { ICategory } from '@/domain/ICategory';
import type { CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection { export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: Category): boolean; areAllScriptsSelected(category: ICategory): boolean;
isAnyScriptSelected(category: Category): boolean; isAnyScriptSelected(category: ICategory): boolean;
} }
export interface CategorySelection extends ReadonlyCategorySelection { export interface CategorySelection extends ReadonlyCategorySelection {

View File

@@ -1,5 +1,3 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
type CategorySelectionStatus = { type CategorySelectionStatus = {
readonly isSelected: true; readonly isSelected: true;
readonly isReverted: boolean; readonly isReverted: boolean;
@@ -8,7 +6,7 @@ type CategorySelectionStatus = {
}; };
export interface CategorySelectionChange { export interface CategorySelectionChange {
readonly categoryId: ExecutableId; readonly categoryId: number;
readonly newStatus: CategorySelectionStatus; readonly newStatus: CategorySelectionStatus;
} }

View File

@@ -1,5 +1,5 @@
import type { Category } from '@/domain/Executables/Category/Category'; import type { ICategory } from '@/domain/ICategory';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
import type { CategorySelection } from './CategorySelection'; import type { CategorySelection } from './CategorySelection';
import type { ScriptSelection } from '../Script/ScriptSelection'; import type { ScriptSelection } from '../Script/ScriptSelection';
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
} }
public areAllScriptsSelected(category: Category): boolean { public areAllScriptsSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -23,11 +23,11 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
return false; return false;
} }
return scripts.every( return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.executableId), (script) => selectedScripts.some((selected) => selected.id === script.id),
); );
} }
public isAnyScriptSelected(category: Category): boolean { public isAnyScriptSelected(category: ICategory): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
const scripts = category.getAllScriptsRecursively(); const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({ .map((script): ScriptSelectionChange => ({
scriptId: script.executableId, scriptId: script.id,
newStatus: { newStatus: {
...change.newStatus, ...change.newStatus,
}, },

View File

@@ -1,10 +1,9 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import type { Script } from '@/domain/Executables/Script/Script'; import type { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { UserSelectedScript } from './UserSelectedScript'; import { UserSelectedScript } from './UserSelectedScript';
import type { ScriptSelection } from './ScriptSelection'; import type { ScriptSelection } from './ScriptSelection';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
@@ -17,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
export class DebouncedScriptSelection implements ScriptSelection { export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<SelectedScript>; private readonly scripts: Repository<string, SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges']; public readonly processChanges: ScriptSelection['processChanges'];
@@ -26,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce, debounce: DebounceFunction = batchedDebounce,
) { ) {
this.scripts = new InMemoryRepository<SelectedScript>(); this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) { for (const script of selectedScripts) {
this.scripts.addItem(script); this.scripts.addItem(script);
} }
@@ -39,8 +38,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
); );
} }
public isSelected(scriptExecutableId: ExecutableId): boolean { public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptExecutableId); return this.scripts.exists(scriptId);
} }
public get selectedScripts(): readonly SelectedScript[] { public get selectedScripts(): readonly SelectedScript[] {
@@ -50,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
public selectAll(): void { public selectAll(): void {
const scriptsToSelect = this.collection const scriptsToSelect = this.collection
.getAllScripts() .getAllScripts()
.filter((script) => !this.scripts.exists(script.executableId)) .filter((script) => !this.scripts.exists(script.id))
.map((script) => new UserSelectedScript(script, false)); .map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) { if (scriptsToSelect.length === 0) {
return; return;
@@ -81,7 +80,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
}); });
} }
public selectOnly(scripts: readonly Script[]): void { public selectOnly(scripts: readonly IScript[]): void {
assertNonEmptyScriptSelection(scripts); assertNonEmptyScriptSelection(scripts);
this.processChanges({ this.processChanges({
changes: [ changes: [
@@ -117,12 +116,12 @@ export class DebouncedScriptSelection implements ScriptSelection {
private applyChange(change: ScriptSelectionChange): number { private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId); const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) { if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted); return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
} }
return this.removeScript(script.executableId); return this.removeScript(script.id);
} }
private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number { private addOrUpdateScript(scriptId: string, revert: boolean): number {
const script = this.collection.getScript(scriptId); const script = this.collection.getScript(scriptId);
const selectedScript = new UserSelectedScript(script, revert); const selectedScript = new UserSelectedScript(script, revert);
if (!this.scripts.exists(selectedScript.id)) { if (!this.scripts.exists(selectedScript.id)) {
@@ -137,7 +136,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
return 1; return 1;
} }
private removeScript(scriptId: ExecutableId): number { private removeScript(scriptId: string): number {
if (!this.scripts.exists(scriptId)) { if (!this.scripts.exists(scriptId)) {
return 0; return 0;
} }
@@ -146,31 +145,31 @@ export class DebouncedScriptSelection implements ScriptSelection {
} }
} }
function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) { function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
if (selectedItems.length === 0) { if (selectedItems.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.'); throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
} }
} }
function getScriptIdsToBeSelected( function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly Script[], desiredScripts: readonly IScript[],
): string[] { ): string[] {
return desiredScripts return desiredScripts
.filter((script) => !existingItems.exists(script.executableId)) .filter((script) => !existingItems.exists(script.id))
.map((script) => script.executableId); .map((script) => script.id);
} }
function getScriptIdsToBeDeselected( function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly Script[], desiredScripts: readonly IScript[],
): string[] { ): string[] {
return existingItems return existingItems
.getItems() .getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId)) .filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
.map((script) => script.id); .map((script) => script.id);
} }
function equals(a: SelectedScript, b: SelectedScript): boolean { function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.executableId === b.script.executableId && a.revert === b.revert; return a.script.equals(b.script.id) && a.revert === b.revert;
} }

View File

@@ -1,17 +1,16 @@
import type { IEventSource } from '@/infrastructure/Events/IEventSource'; import type { IEventSource } from '@/infrastructure/Events/IEventSource';
import type { Script } from '@/domain/Executables/Script/Script'; import type { IScript } from '@/domain/IScript';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
export interface ReadonlyScriptSelection { export interface ReadonlyScriptSelection {
readonly changed: IEventSource<readonly SelectedScript[]>; readonly changed: IEventSource<readonly SelectedScript[]>;
readonly selectedScripts: readonly SelectedScript[]; readonly selectedScripts: readonly SelectedScript[];
isSelected(scriptExecutableId: ExecutableId): boolean; isSelected(scriptId: string): boolean;
} }
export interface ScriptSelection extends ReadonlyScriptSelection { export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly Script[]): void; selectOnly(scripts: readonly IScript[]): void;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void; processChanges(action: ScriptSelectionChangeCommand): void;

View File

@@ -1,5 +1,3 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export type ScriptSelectionStatus = { export type ScriptSelectionStatus = {
readonly isSelected: true; readonly isSelected: true;
readonly isReverted: boolean; readonly isReverted: boolean;
@@ -9,7 +7,7 @@ export type ScriptSelectionStatus = {
}; };
export interface ScriptSelectionChange { export interface ScriptSelectionChange {
readonly scriptId: ExecutableId; readonly scriptId: string;
readonly newStatus: ScriptSelectionStatus; readonly newStatus: ScriptSelectionStatus;
} }

View File

@@ -1,7 +1,9 @@
import type { Script } from '@/domain/Executables/Script/Script'; import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity'; import type { IScript } from '@/domain/IScript';
export interface SelectedScript extends RepositoryEntity { type ScriptId = IScript['id'];
readonly script: Script;
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript;
readonly revert: boolean; readonly revert: boolean;
} }

View File

@@ -1,16 +1,17 @@
import type { Script } from '@/domain/Executables/Script/Script'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity'; import type { IScript } from '@/domain/IScript';
import type { SelectedScript } from './SelectedScript';
export class UserSelectedScript implements RepositoryEntity { type SelectedScriptId = SelectedScript['id'];
public readonly id: string;
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: Script, public readonly script: IScript,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
this.id = script.executableId; super(script.id);
if (revert && !script.canRevert()) { if (revert && !script.canRevert()) {
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`); throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper'; import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection'; import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import type { CategorySelection } from './Category/CategorySelection'; import type { CategorySelection } from './Category/CategorySelection';

View File

@@ -1,48 +1,40 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import WindowsData from '@/application/collections/windows.yaml'; import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml'; import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml'; import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser'; import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser'; import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator'; import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
collectionsData: readonly CollectionData[] = PreParsedCollections, categoryParser = parseCategoryCollection,
utilities: ApplicationParserUtilities = DefaultUtilities, projectDetailsParser = parseProjectDetails,
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
collectionsData = PreParsedCollections,
): IApplication { ): IApplication {
validateCollectionsData(collectionsData, utilities.validator); validateCollectionsData(collectionsData);
const projectDetails = utilities.parseProjectDetails(); const projectDetails = projectDetailsParser(metadata);
const collections = collectionsData.map( const collections = collectionsData.map(
(collection) => utilities.parseCategoryCollection(collection, projectDetails), (collection) => categoryParser(collection, projectDetails),
); );
const app = new Application(projectDetails, collections); const app = new Application(projectDetails, collections);
return app; return app;
} }
const PreParsedCollections: readonly CollectionData[] = [ export type CategoryCollectionParserType
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, LinuxData, WindowsData, MacOsData, LinuxData,
]; ];
function validateCollectionsData( function validateCollectionsData(collections: readonly CollectionData[]) {
collections: readonly CollectionData[], if (!collections.length) {
validator: TypeValidator, throw new Error('missing collections');
) { }
validator.assertNonEmptyCollection({
value: collections,
valueName: 'Collections',
});
} }
interface ApplicationParserUtilities {
readonly parseCategoryCollection: CategoryCollectionParser;
readonly validator: TypeValidator;
readonly parseProjectDetails: ProjectDetailsParser;
}
const DefaultUtilities: ApplicationParserUtilities = {
parseCategoryCollection,
parseProjectDetails,
validator: createTypeValidator(),
};

View File

@@ -1,75 +1,34 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { createEnumParser, type EnumParser } from '../Common/Enum'; import { createEnumParser } from '../Common/Enum';
import { parseCategory, type CategoryParser } from './Executable/CategoryParser'; import { parseCategory } from './CategoryParser';
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator'; import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
export const parseCategoryCollection: CategoryCollectionParser = ( export function parseCategoryCollection(
content,
projectDetails,
utilities: CategoryCollectionParserUtilities = DefaultUtilities,
) => {
validateCollection(content, utilities.validator);
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
const categories = content.actions.map(
(action) => utilities.parseCategory(action, collectionUtilities),
);
const os = utilities.osParser.parseEnum(content.os, 'os');
const collection = utilities.createCategoryCollection({
os, actions: categories, scripting,
});
return collection;
};
export type CategoryCollectionFactory = (
...parameters: ConstructorParameters<typeof CategoryCollection>
) => ICategoryCollection;
export interface CategoryCollectionParser {
(
content: CollectionData,
projectDetails: ProjectDetails,
utilities?: CategoryCollectionParserUtilities,
): ICategoryCollection;
}
function validateCollection(
content: CollectionData, content: CollectionData,
validator: TypeValidator, projectDetails: ProjectDetails,
): void { osParser = createEnumParser(OperatingSystem),
validator.assertObject({ ): ICategoryCollection {
value: content, validate(content);
valueName: 'Collection', const scripting = new ScriptingDefinitionParser()
allowedProperties: [ .parse(content.scripting, projectDetails);
'os', 'scripting', 'actions', 'functions', const context = new CategoryCollectionParseContext(content.functions, scripting);
], const categories = content.actions.map((action) => parseCategory(action, context));
}); const os = osParser.parseEnum(content.os, 'os');
validator.assertNonEmptyCollection({ const collection = new CategoryCollection(
value: content.actions, os,
valueName: '\'actions\' in collection', categories,
}); scripting,
);
return collection;
} }
interface CategoryCollectionParserUtilities { function validate(content: CollectionData): void {
readonly osParser: EnumParser<OperatingSystem>; if (!content.actions.length) {
readonly validator: TypeValidator; throw new Error('content does not define any action');
readonly parseScriptingDefinition: ScriptingDefinitionParser; }
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
readonly parseCategory: CategoryParser;
readonly createCategoryCollection: CategoryCollectionFactory;
} }
const DefaultUtilities: CategoryCollectionParserUtilities = {
osParser: createEnumParser(OperatingSystem),
validator: createTypeValidator(),
parseScriptingDefinition,
createUtilities: createCollectionUtilities,
parseCategory,
createCategoryCollection: (...args) => new CategoryCollection(...args),
};

View File

@@ -0,0 +1,133 @@
import type {
CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { parseDocs } from './DocumentationParser';
import { parseScript } from './Script/ScriptParser';
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
let categoryIdCounter = 0;
export function parseCategory(
category: CategoryData,
context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category {
return parseCategoryRecursively({
categoryData: category,
context,
factory,
});
}
interface ICategoryParseContext {
readonly categoryData: CategoryData,
readonly context: ICategoryCollectionParseContext,
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
subScripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseNode({
nodeData: data,
children,
parent: context.categoryData,
factory: context.factory,
context: context.context,
});
}
try {
return context.factory(
/* id: */ categoryIdCounter++,
/* name: */ context.categoryData.category,
/* docs: */ parseDocs(context.categoryData),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} catch (err) {
return new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
}).throw(err.message);
}
}
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
new NodeValidator({
type: NodeType.Category,
selfNode: category,
parentNode: parentCategory,
})
.assertDefined(category)
.assertValidName(category.category)
.assert(
() => category.children.length > 0,
`"${category.category}" has no children.`,
);
}
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
interface INodeParseContext {
readonly nodeData: CategoryOrScriptData;
readonly children: ICategoryChildren;
readonly parent: CategoryData;
readonly factory: CategoryFactoryType;
readonly context: ICategoryCollectionParseContext;
}
function parseNode(context: INodeParseContext) {
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({
categoryData: context.nodeData,
context: context.context,
factory: context.factory,
parentCategory: context.parent,
});
context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData, context.context);
context.children.subScripts.push(script);
} else {
validator.throw('Node is neither a category or a script.');
}
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
function hasProperty(object: unknown, propertyName: string) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactoryType = (
...parameters: ConstructorParameters<typeof Category>) => Category;
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);

View File

@@ -1,116 +0,0 @@
import { CustomError } from '@/application/Common/CustomError';
import { indentText } from '@/application/Common/Text/IndentText';
export interface ErrorWithContextWrapper {
(
innerError: Error,
additionalContext: string,
): Error;
}
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
innerError,
additionalContext,
) => {
if (!additionalContext) {
throw new Error('Missing additional context');
}
return new ContextualError({
innerError,
additionalContext,
});
};
/**
* Class for building a detailed error trace.
*
* Alternatives considered:
* - `AggregateError`:
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
* - `cause` property:
* Not displayed by all browsers (last tested v126).
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
*
* This is immutable where the constructor sets the values because using getter functions such as
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
* Firefox (last tested v127).
*/
class ContextualError extends CustomError {
constructor(public readonly context: ErrorContext) {
super(
generateDetailedErrorMessageWithContext(context),
{
cause: context.innerError,
},
);
}
}
interface ErrorContext {
readonly innerError: Error;
readonly additionalContext: string;
}
function generateDetailedErrorMessageWithContext(
context: ErrorContext,
): string {
return [
'\n',
// Display the current error message first, then the root cause.
// This prevents repetitive main messages for errors with a `cause:` chain,
// aligning with browser error display conventions.
context.additionalContext,
'\n',
'Error Trace (starting from root cause):',
indentText(
formatErrorTrace(
// Displaying contexts from the top frame (deepest, most recent) aligns with
// common debugger/compiler standard.
extractErrorTraceAscendingFromDeepest(context),
),
),
'\n',
].join('\n');
}
function extractErrorTraceAscendingFromDeepest(
context: ErrorContext,
): string[] {
const originalError = findRootError(context.innerError);
const contextsDescendingFromMostRecent: string[] = [
context.additionalContext,
...gatherContextsFromErrorChain(context.innerError),
originalError.toString(),
];
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
return contextsAscendingFromDeepest;
}
function findRootError(error: Error): Error {
if (error instanceof ContextualError) {
return findRootError(error.context.innerError);
}
return error;
}
function gatherContextsFromErrorChain(
error: Error,
accumulatedContexts: string[] = [],
): string[] {
if (error instanceof ContextualError) {
accumulatedContexts.push(error.context.additionalContext);
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
}
return accumulatedContexts;
}
function formatErrorTrace(
errorMessages: readonly string[],
): string {
if (errorMessages.length === 1) {
return errorMessages[0];
}
return errorMessages
.map((context, index) => `${index + 1}.${indentText(context)}`)
.join('\n');
}

View File

@@ -1,131 +0,0 @@
import type { PropertyKeys } from '@/TypeHelpers';
import {
isNullOrUndefined, isArray, isPlainObject, isString,
} from '@/TypeHelpers';
export interface TypeValidator {
assertObject<T>(assertion: ObjectAssertion<T>): void;
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
}
export interface NonEmptyCollectionAssertion {
readonly value: unknown;
readonly valueName: string;
}
export interface RegexValidationRule {
readonly expectedMatch: RegExp;
readonly errorMessage: string;
}
export interface NonEmptyStringAssertion {
readonly value: unknown;
readonly valueName: string;
readonly rule?: RegexValidationRule;
}
export interface ObjectAssertion<T> {
readonly value: T | unknown;
readonly valueName: string;
readonly allowedProperties?: readonly PropertyKeys<T>[];
}
export function createTypeValidator(): TypeValidator {
return {
assertObject: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertPlainObject(assertion.value, assertion.valueName);
assertNoEmptyProperties(assertion.value, assertion.valueName);
if (assertion.allowedProperties !== undefined) {
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
}
},
assertNonEmptyCollection: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertArray(assertion.value, assertion.valueName);
assertNonEmpty(assertion.value, assertion.valueName);
},
assertNonEmptyString: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertString(assertion.value, assertion.valueName);
if (assertion.value.length === 0) {
throw new Error(`'${assertion.valueName}' is missing.`);
}
if (assertion.rule) {
if (!assertion.value.match(assertion.rule.expectedMatch)) {
throw new Error(assertion.rule.errorMessage);
}
}
},
};
}
function assertDefined<T>(
value: T,
valueName: string,
): asserts value is NonNullable<T> {
if (isNullOrUndefined(value)) {
throw new Error(`'${valueName}' is missing.`);
}
}
function assertPlainObject(
value: unknown,
valueName: string,
): asserts value is object {
if (!isPlainObject(value)) {
throw new Error(`'${valueName}' is not an object.`);
}
}
function assertNoEmptyProperties(
value: object,
valueName: string,
): void {
if (Object.keys(value).length === 0) {
throw new Error(`'${valueName}' is an empty object without properties.`);
}
}
function assertAllowedProperties(
value: object,
valueName: string,
allowedProperties: readonly string[],
): void {
const properties = Object.keys(value).map((p) => p as string);
const disallowedProperties = properties.filter(
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
);
if (disallowedProperties.length > 0) {
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
}
}
function assertArray(
value: unknown,
valueName: string,
): asserts value is Array<unknown> {
if (!isArray(value)) {
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
}
}
function assertString(
value: unknown,
valueName: string,
): asserts value is string {
if (!isString(value)) {
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
}
}
function assertNonEmpty(
value: Array<unknown>,
valueName: string,
): void {
if (value.length === 0) {
throw new Error(`'${valueName}' cannot be an empty array.`);
}
}

View File

@@ -1,7 +1,7 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/'; import type { DocumentableData, DocumentationData } from '@/application/collections/';
import { isString, isArray } from '@/TypeHelpers'; import { isString, isArray } from '@/TypeHelpers';
export const parseDocs: DocsParser = (documentable) => { export function parseDocs(documentable: DocumentableData): readonly string[] {
const { docs } = documentable; const { docs } = documentable;
if (!docs) { if (!docs) {
return []; return [];
@@ -9,12 +9,6 @@ export const parseDocs: DocsParser = (documentable) => {
let result = new DocumentationContainer(); let result = new DocumentationContainer();
result = addDocs(docs, result); result = addDocs(docs, result);
return result.getAll(); return result.getAll();
};
export interface DocsParser {
(
documentable: DocumentableData,
): readonly string[];
} }
function addDocs( function addDocs(
@@ -50,5 +44,5 @@ class DocumentationContainer {
} }
function throwInvalidType(): never { function throwInvalidType(): never {
throw new Error('docs field (documentation) must be a single string or an array of strings.'); throw new Error('docs field (documentation) must be an array of strings');
} }

View File

@@ -1,35 +0,0 @@
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import type { FunctionData } from '@/application/collections/';
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
export interface CategoryCollectionSpecificUtilities {
readonly compiler: IScriptCompiler;
readonly syntax: ILanguageSyntax;
}
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) => {
const syntax = syntaxFactory.create(scripting.language);
return {
compiler: new ScriptCompiler({
functions: functionsData ?? [],
syntax,
}),
syntax,
};
};
export interface CategoryCollectionSpecificUtilitiesFactory {
(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory?: ISyntaxFactory,
): CategoryCollectionSpecificUtilities;
}

View File

@@ -1,181 +0,0 @@
import type {
CategoryData, ScriptData, ExecutableData,
} from '@/application/collections/';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
import { parseDocs, type DocsParser } from './DocumentationParser';
import { parseScript, type ScriptParser } from './Script/ScriptParser';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
import { ExecutableType } from './Validation/ExecutableType';
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
export const parseCategory: CategoryParser = (
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
) => {
return parseCategoryRecursively({
categoryData: category,
collectionUtilities,
categoryUtilities,
});
};
export interface CategoryParser {
(
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities?: CategoryParserUtilities,
): Category;
}
interface CategoryParseContext {
readonly categoryData: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly parentCategory?: CategoryData;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseCategoryRecursively(
context: CategoryParseContext,
): Category | never {
const validator = ensureValidCategory(context);
const children: CategoryChildren = {
subcategories: new Array<Category>(),
subscripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseUnknownExecutable({
data,
children,
parent: context.categoryData,
categoryUtilities: context.categoryUtilities,
collectionUtilities: context.collectionUtilities,
});
}
try {
return context.categoryUtilities.createCategory({
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
name: context.categoryData.category,
docs: context.categoryUtilities.parseDocs(context.categoryData),
subcategories: children.subcategories,
scripts: children.subscripts,
});
} catch (error) {
throw context.categoryUtilities.wrapError(
error,
validator.createContextualErrorMessage('Failed to parse category.'),
);
}
}
function ensureValidCategory(
context: CategoryParseContext,
): ExecutableValidator {
const category = context.categoryData;
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
type: ExecutableType.Category,
self: context.categoryData,
parentCategory: context.parentCategory,
});
validator.assertType((v) => v.assertObject({
value: category,
valueName: `Category '${category.category}'` ?? 'Category',
allowedProperties: [
'docs', 'children', 'category',
],
}));
validator.assertValidName(category.category);
validator.assertType((v) => v.assertNonEmptyCollection({
value: category.children,
valueName: category.category,
}));
return validator;
}
interface CategoryChildren {
readonly subcategories: Category[];
readonly subscripts: Script[];
}
interface ExecutableParseContext {
readonly data: ExecutableData;
readonly children: CategoryChildren;
readonly parent: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseUnknownExecutable(context: ExecutableParseContext) {
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
self: context.data,
parentCategory: context.parent,
});
validator.assertType((v) => v.assertObject({
value: context.data,
valueName: 'Executable',
}));
validator.assert(
() => isCategory(context.data) || isScript(context.data),
'Executable is neither a category or a script.',
);
if (isCategory(context.data)) {
const subCategory = parseCategoryRecursively({
categoryData: context.data,
collectionUtilities: context.collectionUtilities,
parentCategory: context.parent,
categoryUtilities: context.categoryUtilities,
});
context.children.subcategories.push(subCategory);
} else { // A script
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
context.children.subscripts.push(script);
}
}
function isScript(data: ExecutableData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: ExecutableData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
function hasProperty(
object: unknown,
propertyName: string,
): object is NonNullable<object> {
if (typeof object !== 'object') {
return false;
}
if (object === null) { // `typeof object` is `null`
return false;
}
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
interface CategoryParserUtilities {
readonly createCategory: CategoryFactory;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly parseScript: ScriptParser;
readonly parseDocs: DocsParser;
}
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
createCategory,
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
parseScript,
parseDocs,
};

View File

@@ -1,127 +0,0 @@
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
import type { IExpressionParser } from '../IExpressionParser';
import type { IExpression } from '../../Expression/IExpression';
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
export interface RegexParserUtilities {
readonly wrapError: ErrorWithContextWrapper;
readonly createPosition: ExpressionPositionFactory;
readonly createExpression: ExpressionFactory;
readonly createParameterCollection: FunctionParameterCollectionFactory;
}
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
public constructor(
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
) {
}
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
if (!code) {
throw new Error(
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
);
}
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
const matches = this.doOrRethrow(
() => code.matchAll(this.regex),
createErrorContext('Failed to match regex.'),
);
for (const match of matches) {
const primitiveExpression = this.doOrRethrow(
() => this.buildExpression(match),
createErrorContext('Failed to build expression.'),
);
const position = this.doOrRethrow(
() => this.utilities.createPosition(match),
createErrorContext('Failed to create position.'),
);
const parameters = this.doOrRethrow(
() => createParameters(
primitiveExpression,
this.utilities.createParameterCollection(),
),
createErrorContext('Failed to create parameters.'),
);
const expression = this.doOrRethrow(
() => this.utilities.createExpression({
position,
evaluator: primitiveExpression.evaluator,
parameters,
}),
createErrorContext('Failed to create expression.'),
);
yield expression;
}
}
private doOrRethrow<T>(
action: () => T,
context: ErrorContext,
): T {
try {
return action();
} catch (error) {
throw this.utilities.wrapError(
error,
this.buildErrorMessageWithContext(context),
);
}
}
private buildErrorMessageWithContext(context: ErrorContext): string {
return [
context.errorMessage,
`Class name: ${this.constructor.name}`,
`Regex pattern used: ${this.regex}`,
`Code: ${context.code}`,
].join('\n');
}
}
interface ErrorContext {
readonly errorMessage: string,
readonly code: string,
}
function createParameters(
expression: PrimitiveExpression,
parameterCollection: IFunctionParameterCollection,
): IReadOnlyFunctionParameterCollection {
return (expression.parameters || [])
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, parameterCollection);
}
export interface PrimitiveExpression {
readonly evaluator: ExpressionEvaluator;
readonly parameters?: readonly FunctionParameter[];
}
export interface ExpressionFactory {
(
...args: ConstructorParameters<typeof Expression>
): IExpression;
}
const DefaultRegexParserUtilities: RegexParserUtilities = {
wrapError: wrapErrorWithAdditionalContext,
createPosition: createPositionFromRegexFullMatch,
createExpression: (...args) => new Expression(...args),
createParameterCollection: createFunctionParameterCollection,
};

View File

@@ -1,41 +0,0 @@
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
export interface FunctionCallArgument {
readonly parameterName: string;
readonly argumentValue: string;
}
export interface FunctionCallArgumentFactory {
(
parameterName: string,
argumentValue: string,
utilities?: FunctionCallArgumentFactoryUtilities,
): FunctionCallArgument;
}
export const createFunctionCallArgument: FunctionCallArgumentFactory = (
parameterName: string,
argumentValue: string,
utilities: FunctionCallArgumentFactoryUtilities = DefaultUtilities,
): FunctionCallArgument => {
utilities.validateParameterName(parameterName);
utilities.typeValidator.assertNonEmptyString({
value: argumentValue,
valueName: `Function parameter '${parameterName}'`,
});
return {
parameterName,
argumentValue,
};
};
interface FunctionCallArgumentFactoryUtilities {
readonly typeValidator: TypeValidator;
readonly validateParameterName: ParameterNameValidator;
}
const DefaultUtilities: FunctionCallArgumentFactoryUtilities = {
typeValidator: createTypeValidator(),
validateParameterName,
};

View File

@@ -1,80 +0,0 @@
import type {
FunctionCallData,
FunctionCallsData,
FunctionCallParametersData,
} from '@/application/collections/';
import { isArray, isPlainObject } from '@/TypeHelpers';
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { ParsedFunctionCall } from './ParsedFunctionCall';
import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument';
import type { FunctionCall } from './FunctionCall';
export interface FunctionCallsParser {
(
calls: FunctionCallsData,
utilities?: FunctionCallParsingUtilities,
): FunctionCall[];
}
interface FunctionCallParsingUtilities {
readonly typeValidator: TypeValidator;
readonly createCallArgument: FunctionCallArgumentFactory;
}
const DefaultUtilities: FunctionCallParsingUtilities = {
typeValidator: createTypeValidator(),
createCallArgument: createFunctionCallArgument,
};
export const parseFunctionCalls: FunctionCallsParser = (
calls,
utilities = DefaultUtilities,
) => {
const sequence = getCallSequence(calls, utilities.typeValidator);
return sequence.map((call) => parseFunctionCall(call, utilities));
};
function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] {
if (!isPlainObject(calls) && !isArray(calls)) {
throw new Error('called function(s) must be an object or array');
}
if (isArray(calls)) {
validator.assertNonEmptyCollection({
value: calls,
valueName: 'Function call sequence',
});
return calls as FunctionCallData[];
}
const singleCall = calls as FunctionCallData;
return [singleCall];
}
function parseFunctionCall(
call: FunctionCallData,
utilities: FunctionCallParsingUtilities,
): FunctionCall {
utilities.typeValidator.assertObject({
value: call,
valueName: 'Function call',
allowedProperties: ['function', 'parameters'],
});
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
return new ParsedFunctionCall(call.function, callArgs);
}
function parseArgs(
parameters: FunctionCallParametersData | undefined,
createArgument: FunctionCallArgumentFactory,
): FunctionCallArgumentCollection {
const parametersMap = parameters ?? {};
return Object.keys(parametersMap)
.map((parameterName) => {
const argumentValue = parametersMap[parameterName];
return createArgument(parameterName, argumentValue);
})
.reduce((args, arg) => {
args.addArgument(arg);
return args;
}, new FunctionCallArgumentCollection());
}

View File

@@ -1,12 +0,0 @@
import { FunctionParameterCollection } from './FunctionParameterCollection';
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
export interface FunctionParameterCollectionFactory {
(
...args: ConstructorParameters<typeof FunctionParameterCollection>
): IFunctionParameterCollection;
}
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
return new FunctionParameterCollection(...args);
};

View File

@@ -1,21 +0,0 @@
import type { ParameterDefinitionData } from '@/application/collections/';
import { validateParameterName, type ParameterNameValidator } from '../Shared/ParameterNameValidator';
import type { FunctionParameter } from './FunctionParameter';
export interface FunctionParameterParser {
(
data: ParameterDefinitionData,
validator?: ParameterNameValidator,
): FunctionParameter;
}
export const parseFunctionParameter: FunctionParameterParser = (
data,
validator = validateParameterName,
) => {
validator(data.name);
return {
name: data.name,
isOptional: data.optional || false,
};
};

View File

@@ -1,9 +0,0 @@
import type { FunctionParameter } from './FunctionParameter';
export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly FunctionParameter[];
}
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: FunctionParameter): void;
}

View File

@@ -1,22 +0,0 @@
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
export interface ParameterNameValidator {
(
parameterName: string,
typeValidator?: TypeValidator,
): void;
}
export const validateParameterName = (
parameterName: string,
typeValidator = createTypeValidator(),
) => {
typeValidator.assertNonEmptyString({
value: parameterName,
valueName: 'Parameter name',
rule: {
expectedMatch: /^[0-9a-zA-Z]+$/,
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
},
});
};

View File

@@ -1,86 +0,0 @@
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
import type { IScriptCompiler } from './IScriptCompiler';
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
interface ScriptCompilerUtilities {
readonly sharedFunctionsParser: SharedFunctionsParser;
readonly callCompiler: FunctionCallCompiler;
readonly codeValidator: ICodeValidator;
readonly wrapError: ErrorWithContextWrapper;
readonly scriptCodeFactory: ScriptCodeFactory;
}
const DefaultUtilities: ScriptCompilerUtilities = {
sharedFunctionsParser: parseSharedFunctions,
callCompiler: FunctionCallSequenceCompiler.instance,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
scriptCodeFactory: createScriptCode,
};
interface CategoryCollectionDataContext {
readonly functions: readonly FunctionData[];
readonly syntax: ILanguageSyntax;
}
export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
constructor(
categoryContext: CategoryCollectionDataContext,
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
) {
this.functions = this.utilities.sharedFunctionsParser(
categoryContext.functions,
categoryContext.syntax,
);
}
public canCompile(script: ScriptData): boolean {
return hasCall(script);
}
public compile(script: ScriptData): ScriptCode {
try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call);
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.utilities.codeValidator);
return this.utilities.scriptCodeFactory(
compiledCode.code,
compiledCode.revertCode,
);
} catch (error) {
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
}
}
}
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
filterEmptyStrings([compiledCode.code, compiledCode.revertCode])
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
);
}
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
}

View File

@@ -1,144 +0,0 @@
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import type { Script } from '@/domain/Executables/Script/Script';
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
import { parseDocs, type DocsParser } from '../DocumentationParser';
import { ExecutableType } from '../Validation/ExecutableType';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
import type { CategoryCollectionSpecificUtilities } from '../CategoryCollectionSpecificUtilities';
export interface ScriptParser {
(
data: ScriptData,
collectionUtilities: CategoryCollectionSpecificUtilities,
scriptUtilities?: ScriptParserUtilities,
): Script;
}
export const parseScript: ScriptParser = (
data,
collectionUtilities,
scriptUtilities = DefaultUtilities,
) => {
const validator = scriptUtilities.createValidator({
type: ExecutableType.Script,
self: data,
});
validateScript(data, validator);
try {
const script = scriptUtilities.createScript({
executableId: data.name, // Pseudo-ID for uniqueness until real ID support
name: data.name,
code: parseCode(
data,
collectionUtilities,
scriptUtilities.codeValidator,
scriptUtilities.createCode,
),
docs: scriptUtilities.parseDocs(data),
level: parseLevel(data.recommend, scriptUtilities.levelParser),
});
return script;
} catch (error) {
throw scriptUtilities.wrapError(
error,
validator.createContextualErrorMessage('Failed to parse script.'),
);
}
};
function parseLevel(
level: string | undefined,
parser: EnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) {
return undefined;
}
return parser.parseEnum(level, 'level');
}
function parseCode(
script: ScriptData,
collectionUtilities: CategoryCollectionSpecificUtilities,
codeValidator: ICodeValidator,
createCode: ScriptCodeFactory,
): ScriptCode {
if (collectionUtilities.compiler.canCompile(script)) {
return collectionUtilities.compiler.compile(script);
}
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
const code = createCode(codeScript.code, codeScript.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionUtilities.syntax);
return code;
}
function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode,
validator: ICodeValidator,
syntax: ILanguageSyntax,
) {
filterEmptyStrings([scriptCode.execute, scriptCode.revert])
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
),
);
}
function validateScript(
script: ScriptData,
validator: ExecutableValidator,
): asserts script is NonNullable<ScriptData> {
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
value: script,
valueName: `Script '${script.name}'` ?? 'Script',
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
}));
validator.assertValidName(script.name);
validator.assert(
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
'Neither "call" or "code" is defined.',
);
validator.assert(
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
'Both "call" and "code" are defined.',
);
validator.assert(
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
'Both "call" and "revertCode" are defined.',
);
}
interface ScriptParserUtilities {
readonly levelParser: EnumParser<RecommendationLevel>;
readonly createScript: ScriptFactory;
readonly codeValidator: ICodeValidator;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly createCode: ScriptCodeFactory;
readonly parseDocs: DocsParser;
}
const DefaultUtilities: ScriptParserUtilities = {
levelParser: createEnumParser(RecommendationLevel),
createScript,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
createCode: createScriptCode,
parseDocs,
};

View File

@@ -1,24 +0,0 @@
import type { CategoryData, ScriptData, ExecutableData } from '@/application/collections/';
import { ExecutableType } from './ExecutableType';
export type ExecutableErrorContext = {
readonly parentCategory?: CategoryData;
} & (CategoryErrorContext | ScriptErrorContext | UnknownExecutableErrorContext);
export type CategoryErrorContext = {
readonly type: ExecutableType.Category;
readonly self: CategoryData;
readonly parentCategory?: CategoryData;
};
export type ScriptErrorContext = {
readonly type: ExecutableType.Script;
readonly self: ScriptData;
readonly parentCategory?: CategoryData;
};
export type UnknownExecutableErrorContext = {
readonly type?: undefined;
readonly self: ExecutableData;
readonly parentCategory?: CategoryData;
};

View File

@@ -1,43 +0,0 @@
import type { ExecutableData } from '@/application/collections/';
import { ExecutableType } from './ExecutableType';
import type { ExecutableErrorContext } from './ExecutableErrorContext';
export interface ExecutableContextErrorMessageCreator {
(
errorMessage: string,
context: ExecutableErrorContext,
): string;
}
export const createExecutableContextErrorMessage: ExecutableContextErrorMessageCreator = (
errorMessage,
context,
) => {
let message = '';
if (context.type !== undefined) {
message += `${ExecutableType[context.type]}: `;
}
message += errorMessage;
message += `\n\n${getErrorContextDetails(context)}`;
return message;
};
function getErrorContextDetails(context: ExecutableErrorContext): string {
let output = `Executable: ${formatExecutable(context.self)}`;
if (context.parentCategory) {
output += `\n\nParent category: ${formatExecutable(context.parentCategory)}`;
}
return output;
}
function formatExecutable(executable: ExecutableData): string {
if (!executable) {
return 'Executable data is missing.';
}
const maxLength = 1000;
let output = JSON.stringify(executable, undefined, 2);
if (output.length > maxLength) {
output = `${output.substring(0, maxLength)}\n... [Rest of the executable trimmed]`;
}
return output;
}

View File

@@ -1,4 +0,0 @@
export enum ExecutableType {
Script,
Category,
}

View File

@@ -1,69 +0,0 @@
import { isString } from '@/TypeHelpers';
import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator';
import { type ExecutableErrorContext } from './ExecutableErrorContext';
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
export interface ExecutableValidatorFactory {
(context: ExecutableErrorContext): ExecutableValidator;
}
type AssertTypeFunction = (validator: TypeValidator) => void;
export interface ExecutableValidator {
assertValidName(nameValue: string): void;
assertType(assert: AssertTypeFunction): void;
assert(
validationPredicate: () => boolean,
errorMessage: string,
): asserts validationPredicate is (() => true);
createContextualErrorMessage(errorMessage: string): string;
}
export const createExecutableDataValidator
: ExecutableValidatorFactory = (context) => new ContextualExecutableValidator(context);
export class ContextualExecutableValidator implements ExecutableValidator {
constructor(
private readonly context: ExecutableErrorContext,
private readonly createErrorMessage
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
private readonly validator: TypeValidator = createTypeValidator(),
) {
}
public assertValidName(nameValue: string): void {
this.assert(() => Boolean(nameValue), 'missing name');
this.assert(
() => isString(nameValue),
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
);
}
public assertType(assert: AssertTypeFunction): void {
try {
assert(this.validator);
} catch (error) {
this.throw(error.message);
}
}
public assert(
validationPredicate: () => boolean,
errorMessage: string,
): asserts validationPredicate is (() => true) {
if (!validationPredicate()) {
this.throw(errorMessage);
}
}
public createContextualErrorMessage(errorMessage: string): string {
return this.createErrorMessage(errorMessage, this.context);
}
private throw(errorMessage: string): never {
throw new Error(
this.createContextualErrorMessage(errorMessage),
);
}
}

View File

@@ -0,0 +1,3 @@
import type { ScriptData, CategoryData } from '@/application/collections/';
export type NodeData = CategoryData | ScriptData;

View File

@@ -0,0 +1,34 @@
import { CustomError } from '@/application/Common/CustomError';
import { NodeType } from './NodeType';
import type { NodeData } from './NodeData';
export class NodeDataError extends CustomError {
constructor(message: string, public readonly context: INodeDataErrorContext) {
super(createMessage(message, context));
}
}
export interface INodeDataErrorContext {
readonly type?: NodeType;
readonly selfNode: NodeData;
readonly parentNode?: NodeData;
}
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
let message = '';
if (context.type !== undefined) {
message += `${NodeType[context.type]}: `;
}
message += errorMessage;
message += `\n${dump(context)}`;
return message;
}
function dump(context: INodeDataErrorContext): string {
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
let output = `Self: ${printJson(context.selfNode)}`;
if (context.parentNode) {
output += `\nParent: ${printJson(context.parentNode)}`;
}
return output;
}

View File

@@ -0,0 +1,4 @@
export enum NodeType {
Script,
Category,
}

View File

@@ -0,0 +1,39 @@
import { isString } from '@/TypeHelpers';
import { type INodeDataErrorContext, NodeDataError } from './NodeDataError';
import type { NodeData } from './NodeData';
export class NodeValidator {
constructor(private readonly context: INodeDataErrorContext) {
}
public assertValidName(nameValue: string) {
return this
.assert(
() => Boolean(nameValue),
'missing name',
)
.assert(
() => isString(nameValue),
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
);
}
public assertDefined(node: NodeData) {
return this.assert(
() => node !== undefined && node !== null && Object.keys(node).length > 0,
'missing node data',
);
}
public assert(validationPredicate: () => boolean, errorMessage: string) {
if (!validationPredicate()) {
this.throw(errorMessage);
}
return this;
}
public throw(errorMessage: string): never {
throw new NodeDataError(errorMessage, this.context);
}
}

View File

@@ -24,10 +24,6 @@ parseProjectDetails(
); );
} }
export interface ProjectDetailsParser {
(): ProjectDetails;
}
export type ProjectDetailsFactory = ( export type ProjectDetailsFactory = (
...args: ConstructorArguments<typeof GitHubProjectDetails> ...args: ConstructorArguments<typeof GitHubProjectDetails>
) => ProjectDetails; ) => ProjectDetails;

View File

@@ -0,0 +1,23 @@
import type { FunctionData } from '@/application/collections/';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
import type { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
import type { IScriptCompiler } from './Compiler/IScriptCompiler';
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
import type { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
public readonly compiler: IScriptCompiler;
public readonly syntax: ILanguageSyntax;
constructor(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
}
}

View File

@@ -1,4 +1,4 @@
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
@@ -7,18 +7,15 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
import type { IExpression } from './IExpression'; import type { IExpression } from './IExpression';
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string; export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression { export class Expression implements IExpression {
public readonly parameters: IReadOnlyFunctionParameterCollection; public readonly parameters: IReadOnlyFunctionParameterCollection;
public readonly position: ExpressionPosition; constructor(
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator; public readonly evaluator: ExpressionEvaluator,
parameters?: IReadOnlyFunctionParameterCollection,
constructor(parameters: ExpressionInitParameters) { ) {
this.parameters = parameters.parameters ?? new FunctionParameterCollection(); this.parameters = parameters ?? new FunctionParameterCollection();
this.evaluator = parameters.evaluator;
this.position = parameters.position;
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
@@ -29,12 +26,6 @@ export class Expression implements IExpression {
} }
} }
export interface ExpressionInitParameters {
readonly position: ExpressionPosition,
readonly evaluator: ExpressionEvaluator,
readonly parameters?: IReadOnlyFunctionParameterCollection,
}
function validateThatAllRequiredParametersAreSatisfied( function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,

View File

@@ -1,13 +1,8 @@
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
export interface ExpressionPositionFactory { export function createPositionFromRegexFullMatch(
( match: RegExpMatchArray,
match: RegExpMatchArray, ): ExpressionPosition {
): ExpressionPosition
}
export const createPositionFromRegexFullMatch
: ExpressionPositionFactory = (match) => {
const startPos = match.index; const startPos = match.index;
if (startPos === undefined) { if (startPos === undefined) {
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`); throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
@@ -18,4 +13,4 @@ export const createPositionFromRegexFullMatch
} }
const endPos = startPos + fullMatch.length; const endPos = startPos + fullMatch.length;
return new ExpressionPosition(startPos, endPos); return new ExpressionPosition(startPos, endPos);
}; }

View File

@@ -1,4 +1,4 @@
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import type { IExpressionsCompiler } from './IExpressionsCompiler'; import type { IExpressionsCompiler } from './IExpressionsCompiler';

View File

@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
import type { IExpression } from '../Expression/IExpression'; import type { IExpression } from '../Expression/IExpression';
import type { IExpressionParser } from './IExpressionParser'; import type { IExpressionParser } from './IExpressionParser';
const Parsers: readonly IExpressionParser[] = [ const Parsers = [
new ParameterSubstitutionParser(), new ParameterSubstitutionParser(),
new WithParser(), new WithParser(),
] as const; ];
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) {

View File

@@ -0,0 +1,53 @@
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
import type { IExpressionParser } from '../IExpressionParser';
import type { IExpression } from '../../Expression/IExpression';
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
if (!code) {
throw new Error('missing code');
}
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
}
}
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
try {
return action();
} catch (error) {
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
}
}
}
function createParameters(
expression: IPrimitiveExpression,
): FunctionParameterCollection {
return (expression.parameters || [])
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, new FunctionParameterCollection());
}
export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator;
parameters?: readonly IFunctionParameter[];
}

View File

@@ -1,4 +1,4 @@
export interface Pipe { export interface IPipe {
readonly name: string; readonly name: string;
apply(input: string): string; apply(input: string): string;
} }

View File

@@ -1,11 +1,11 @@
import type { Pipe } from '../Pipe'; import type { IPipe } from '../IPipe';
export class EscapeDoubleQuotes implements Pipe { export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes'; public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string { public apply(raw: string): string {
if (!raw) { if (!raw) {
return ''; return raw;
} }
return raw.replaceAll('"', '"^""'); return raw.replaceAll('"', '"^""');
/* eslint-disable vue/max-len */ /* eslint-disable vue/max-len */

View File

@@ -1,7 +1,6 @@
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines'; import type { IPipe } from '../IPipe';
import type { Pipe } from '../Pipe';
export class InlinePowerShell implements Pipe { export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell'; public readonly name: string = 'inlinePowerShell';
public apply(code: string): string { public apply(code: string): string {
@@ -9,11 +8,9 @@ export class InlinePowerShell implements Pipe {
return code; return code;
} }
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent" const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
// Order is important
inlineComments, inlineComments,
mergeHereStrings,
mergeLinesWithBacktick, mergeLinesWithBacktick,
mergeLinesWithBracketCodeBlocks, mergeHereStrings,
mergeNewLines, mergeNewLines,
]).reduce((a, b) => (data) => b(a(data))); ]).reduce((a, b) => (data) => b(a(data)));
const newCode = processor(code); const newCode = processor(code);
@@ -92,6 +89,10 @@ function inlineComments(code: string): string {
*/ */
} }
function getLines(code: string): string[] {
return (code?.split(/\r\n|\r|\n/) || []);
}
/* /*
Merges inline here-strings to a single lined string with Windows line terminator (\r\n) Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
@@ -101,18 +102,18 @@ function mergeHereStrings(code: string) {
return code.replaceAll(regex, (_$, quotes, scope) => { return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes); const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes); const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = splitTextIntoLines(escaped); const lines = getLines(escaped);
const inlined = lines.join(newString.separator); const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`; const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted; return quoted;
}); });
} }
interface InlinedHereString { interface IInlinedHereString {
readonly quotesAround: string; readonly quotesAround: string;
readonly escapedQuotes: string; readonly escapedQuotes: string;
readonly separator: string; readonly separator: string;
} }
function getHereStringHandler(quotes: string): InlinedHereString { function getHereStringHandler(quotes: string): IInlinedHereString {
/* /*
We handle @' and @" differently. We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable. Single quotes are interpreted literally and doubles are expandable.
@@ -157,33 +158,9 @@ function mergeLinesWithBacktick(code: string) {
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' '); return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
} }
/**
* Inlines code blocks in PowerShell scripts while preserving correct syntax.
* It removes unnecessary newlines and spaces around brackets,
* inlining the code where possible.
* This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
*/
function mergeLinesWithBracketCodeBlocks(code: string): string {
return code
// Opening bracket: [whitespace] Opening bracket (newline)
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
}
function mergeNewLines(code: string) { function mergeNewLines(code: string) {
const nonEmptyLines = splitTextIntoLines(code) return getLines(code)
.map((line) => line.trim()) .map((line) => line.trim())
.filter((line) => line.length > 0); .filter((line) => line.length > 0)
.join('; ');
return nonEmptyLines
.map((line, index) => {
const isLastLine = index === nonEmptyLines.length - 1;
if (isLastLine) {
return line;
}
return line.endsWith(';') ? line : `${line};`;
})
.join(' ');
} }

View File

@@ -1,6 +1,6 @@
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell'; import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes'; import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
import type { Pipe } from './Pipe'; import type { IPipe } from './IPipe';
const RegisteredPipes = [ const RegisteredPipes = [
new EscapeDoubleQuotes(), new EscapeDoubleQuotes(),
@@ -8,19 +8,19 @@ const RegisteredPipes = [
]; ];
export interface IPipeFactory { export interface IPipeFactory {
get(pipeName: string): Pipe; get(pipeName: string): IPipe;
} }
export class PipeFactory implements IPipeFactory { export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, Pipe>(); private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly Pipe[] = RegisteredPipes) { constructor(pipes: readonly IPipe[] = RegisteredPipes) {
for (const pipe of pipes) { for (const pipe of pipes) {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
} }
public get(pipeName: string): Pipe { public get(pipeName: string): IPipe {
validatePipeName(pipeName); validatePipeName(pipeName);
const pipe = this.pipes.get(pipeName); const pipe = this.pipes.get(pipeName);
if (!pipe) { if (!pipe) {
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
return pipe; return pipe;
} }
private registerPipe(pipe: Pipe): void { private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name); validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) { if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`); throw new Error(`Pipe name must be unique: "${pipe.name}"`);

View File

@@ -1,4 +1,5 @@
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser'; import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser { export class ParameterSubstitutionParser extends RegexParser {
@@ -11,14 +12,11 @@ export class ParameterSubstitutionParser extends RegexParser {
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression { protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1]; const parameterName = match[1];
const pipeline = match[2]; const pipeline = match[2];
return { return {
parameters: [{ parameters: [new FunctionParameter(parameterName, false)],
name: parameterName,
isOptional: false,
}],
evaluator: (context) => { evaluator: (context) => {
const { argumentValue } = context.args.getArgument(parameterName); const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) { if (!pipeline) {

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory'; import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
@@ -83,10 +84,7 @@ class WithStatementBuilder {
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression { public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
const parameters = new FunctionParameterCollection(); const parameters = new FunctionParameterCollection();
parameters.addParameter({ parameters.addParameter(new FunctionParameter(this.parameterName, true));
name: this.parameterName,
isOptional: true,
});
const position = new ExpressionPosition( const position = new ExpressionPosition(
this.startExpressionPosition.start, this.startExpressionPosition.start,
endExpressionPosition.end, endExpressionPosition.end,

View File

@@ -0,0 +1,14 @@
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
import type { IFunctionCallArgument } from './IFunctionCallArgument';
export class FunctionCallArgument implements IFunctionCallArgument {
constructor(
public readonly parameterName: string,
public readonly argumentValue: string,
) {
ensureValidParameterName(parameterName);
if (!argumentValue) {
throw new Error(`missing argument value for "${parameterName}"`);
}
}
}

View File

@@ -1,10 +1,10 @@
import type { FunctionCallArgument } from './FunctionCallArgument'; import type { IFunctionCallArgument } from './IFunctionCallArgument';
import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection'; import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection { export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, FunctionCallArgument>(); private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: FunctionCallArgument): void { public addArgument(argument: IFunctionCallArgument): void {
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`);
} }
@@ -22,7 +22,7 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
return this.arguments.has(parameterName); return this.arguments.has(parameterName);
} }
public getArgument(parameterName: string): FunctionCallArgument { public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) { if (!parameterName) {
throw new Error('missing parameter name'); throw new Error('missing parameter name');
} }

View File

@@ -0,0 +1,4 @@
export interface IFunctionCallArgument {
readonly parameterName: string;
readonly argumentValue: string;
}

View File

@@ -1,11 +1,11 @@
import type { FunctionCallArgument } from './FunctionCallArgument'; import type { IFunctionCallArgument } from './IFunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection { export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): FunctionCallArgument; getArgument(parameterName: string): IFunctionCallArgument;
getAllParameterNames(): string[]; getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean; hasArgument(parameterName: string): boolean;
} }
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection { export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: FunctionCallArgument): void; addArgument(argument: IFunctionCallArgument): void;
} }

View File

@@ -1,4 +1,3 @@
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
import type { CompiledCode } from '../CompiledCode'; import type { CompiledCode } from '../CompiledCode';
import type { CodeSegmentMerger } from './CodeSegmentMerger'; import type { CodeSegmentMerger } from './CodeSegmentMerger';
@@ -9,9 +8,11 @@ export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
} }
return { return {
code: joinCodeParts(codeSegments.map((f) => f.code)), code: joinCodeParts(codeSegments.map((f) => f.code)),
revertCode: joinCodeParts(filterEmptyStrings( revertCode: joinCodeParts(
codeSegments.map((f) => f.revertCode), codeSegments
)), .map((f) => f.revertCode)
.filter((code): code is string => Boolean(code)),
),
}; };
} }
} }

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import type { FunctionCall } from '../FunctionCall'; import type { FunctionCall } from '../FunctionCall';
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler'; import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import type { CompiledCode } from './CompiledCode'; import type { CompiledCode } from './CompiledCode';
import type { FunctionCall } from '../FunctionCall'; import type { FunctionCall } from '../FunctionCall';

View File

@@ -1,4 +1,4 @@
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger'; import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler'; import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection';

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