Compare commits

..

1 Commits

Author SHA1 Message Date
undergroundwires
0c8412c467 Improve disabling of Windows services
- Unify way of disabling Windows services using templating.
- Remove false-positive error messages.
- Use smarter logic to start/stop service.

Logic improvements include:
  - Check if service is running before stopping/starting the service.
  - Do not start the service it's not an Automatic service.
  - Do not write stderr if service cannot be stopped/started as it's not
    not the main goal of the function.
  - Check whether service is already disabled.

Add more documentation about the disabled Windows service.

Script: Disable diagnostics telemetry
 - Add missing revert script
 - Add more granular control for each service.
2021-11-21 20:58:44 +01:00
366 changed files with 31755 additions and 37563 deletions

View File

@@ -1,7 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

View File

@@ -1,115 +0,0 @@
const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style');
module.exports = {
root: true,
env: {
node: true,
},
extends: [
// Vue specific rules, eslint-plugin-vue
// Added by Vue CLI
'plugin:vue/essential',
// Extends eslint-config-airbnb
// Added by Vue CLI
// Here until https://github.com/vuejs/eslint-config-airbnb/issues/23 is done
'@vue/airbnb',
// Extends @typescript-eslint/recommended
// Uses the recommended rules from the @typescript-eslint/eslint-plugin
// Added by Vue CLI
'@vue/typescript/recommended',
],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
...getOwnRules(),
...getTurnedOffBrokenRules(),
...getOpinionatedRuleOverrides(),
...getTodoRules(),
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)',
],
env: {
mocha: true,
},
},
{
files: ['**/tests/**/*.{j,t}s?(x)'],
rules: {
'no-console': 'off',
},
},
],
};
function getOwnRules() {
return {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'linebreak-style': ['error', 'unix'], // This is also enforced in .editorconfig and .gitattributes files
'import/order': [ // Enforce strict import order taking account into aliases
'error',
{
groups: [ // Enforce more strict order than AirBnb
'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external"
'@/**', // @/..
'@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well)
'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/..
].map((pattern) => ({ pattern, group: 'internal' })),
},
],
};
}
function getTodoRules() { // Should be worked on separate future commits
return {
'import/no-extraneous-dependencies': 'off',
// Requires webpack configuration change with import '..yaml' files.
'import/no-webpack-loader-syntax': 'off',
'import/extensions': 'off',
'import/no-unresolved': 'off',
// Accessibility improvements:
'vuejs-accessibility/form-control-has-label': 'off',
'vuejs-accessibility/click-events-have-key-events': 'off',
'vuejs-accessibility/anchor-has-content': 'off',
'vuejs-accessibility/accessible-emoji': 'off',
};
}
function getTurnedOffBrokenRules() {
return {
// Broken in TypeScript
'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors
'no-shadow': 'off', // Fails with TypeScript enums
};
}
function getOpinionatedRuleOverrides() {
return {
// https://erkinekici.com/articles/linting-trap#no-use-before-define
'no-use-before-define': 'off',
// https://erkinekici.com/articles/linting-trap#arrow-body-style
'arrow-body-style': 'off',
// https://erkinekici.com/articles/linting-trap#no-plusplus
'no-plusplus': 'off',
// https://erkinekici.com/articles/linting-trap#no-param-reassign
'no-param-reassign': 'off',
// https://erkinekici.com/articles/linting-trap#class-methods-use-this
'class-methods-use-this': 'off',
// https://erkinekici.com/articles/linting-trap#importprefer-default-export
'import/prefer-default-export': 'off',
// https://erkinekici.com/articles/linting-trap#disallowing-for-of
// Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351
'no-restricted-syntax': [
baseStyleRules['no-restricted-syntax'][0],
...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'),
],
};
}

6
.gitattributes vendored
View File

@@ -1,6 +0,0 @@
# Prevent Git from auto-converting to CRLF on Windows, and convert to LF on checkin.
# * : All files
# text=auto : If Git decides content it text, it converts CRLF to LF on checkin.
# eol=lf : forces Git to normalize line endings to LF on checkin and prevents conversion
# to CRLF when the file is checked out.
* text=auto eol=lf

View File

@@ -1,4 +1,4 @@
name: release-git
name: Bump & release
on:
push: # Ensure a new release is created for each new tag

View File

@@ -1,59 +0,0 @@
name: build-checks
on:
push:
pull_request:
jobs:
build-web:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [ development, test, production ]
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Build
run: npm run build -- --mode ${{ matrix.mode }}
# A new job is used due to environments/modes different from Vue CLI, https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
build-desktop:
strategy:
matrix:
os: [ macos, ubuntu, windows ]
mode: [ development, production ] # "test" is not supported https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1627
fail-fast: false # Allows to see results from other combinations
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Install cross-env
# Used to set NODE_ENV due to https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/1626
run: npm install --global cross-env
-
name: Build
run: |-
cross-env-shell NODE_ENV=${{ matrix.mode }}
npm run electron:build -- --publish never --mode ${{ matrix.mode }}

View File

@@ -1,4 +1,4 @@
name: release-desktop
name: Deploy desktop
on:
release:

View File

@@ -1,4 +1,4 @@
name: release-site
name: Deploy site
on:
release:

View File

@@ -1,4 +1,4 @@
name: quality-checks
name: Quality checks
on: [ push, pull_request ]
@@ -8,13 +8,12 @@ jobs:
strategy:
matrix:
lint-command:
- npm run lint:eslint
- npm run lint:vue
- npm run lint:yaml
- npm run lint:md
- npm run lint:md:relative-urls
- npm run lint:md:consistency
os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations
fail-fast: false # So it continues with other commands if one fails
steps:
- name: Checkout
uses: actions/checkout@v2

View File

@@ -1,4 +1,4 @@
name: security-checks
name: Security checks
on:
push:

View File

@@ -1,9 +1,9 @@
name: integration-tests
name: Test
on:
push:
pull_request:
schedule: # To get notified about problems from third party dependencies
schedule: # for integration tests
- cron: '0 0 * * 0' # at 00:00 on every Sunday
jobs:
@@ -25,6 +25,9 @@ jobs:
-
name: Install dependencies
run: npm ci
-
name: Run unit tests
run: npm run test:unit
-
name: Run integration tests
run: npm run test:integration

View File

@@ -1,28 +0,0 @@
name: e2e-tests
on:
push:
pull_request:
jobs:
run-tests:
strategy:
matrix:
os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Run e2e tests
run: npm run test:e2e -- --headless

View File

@@ -1,28 +0,0 @@
name: unit-tests
on:
push:
pull_request:
jobs:
run-tests:
strategy:
matrix:
os: [macos, ubuntu, windows]
fail-fast: false # So it still runs on other OSes if one of them fails
runs-on: ${{ matrix.os }}-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: 15.x
-
name: Install dependencies
run: npm ci
-
name: Run unit tests
run: npm run test:unit

5
.gitignore vendored
View File

@@ -3,7 +3,4 @@ dist/
.vs
.vscode
#Electron-builder output
/dist_electron
# Cypress
/tests/e2e/screenshots
/tests/e2e/videos
/dist_electron

View File

@@ -1,20 +1,5 @@
# Changelog
## 0.11.2 (2021-12-03)
* Fix Windows TrustedInstaller session errors | [20a0071](https://github.com/undergroundwires/privacy.sexy/commit/20a0071c0d3d769a8f31218abdbfc4cafa25c6ff)
* Improve tests for `UserSelection` | [2f90cac](https://github.com/undergroundwires/privacy.sexy/commit/2f90cac52ab9e57615aeec41f9daa842bce770a5)
* Fix disabling/enabling Defender on Windows #104 | [2e08293](https://github.com/undergroundwires/privacy.sexy/commit/2e082932c952b0849ab2b8709ff0c75293b88e95)
* Refactor Saas naming, structure and modules | [bf83c58](https://github.com/undergroundwires/privacy.sexy/commit/bf83c58982ffa178facc6d35e50c7f1eac7ff236)
* Fix Defender features errors in Windows #104 | [d7761ab](https://github.com/undergroundwires/privacy.sexy/commit/d7761ab30e7f1e10a2919c196804d67511d6163a)
* Fix unintendedly inlined Windows scripts | [f2d9881](https://github.com/undergroundwires/privacy.sexy/commit/f2d988138257ff184884e4adc83c39e3bc247e9b)
* Fix Defender error due to non-english Windows #104 | [7c02ffb](https://github.com/undergroundwires/privacy.sexy/commit/7c02ffb6c95382b94f0b05e6f259cc418ec91c93)
* Improve and unify disabling of Windows services | [70cdf38](https://github.com/undergroundwires/privacy.sexy/commit/70cdf3865a0de3214fc9e26fbdada4b0cb413c46)
* Improve Windows defender docs and errors #104 | [d2518b1](https://github.com/undergroundwires/privacy.sexy/commit/d2518b11a7774ec58b9b46a691e2f013855bf0f9)
* Unrecommend and complete Windows Push Notif. #101 | [c65209e](https://github.com/undergroundwires/privacy.sexy/commit/c65209e6a99230f15ace8955e8d5a6f3333d146b)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.11.1...0.11.2)
## 0.11.1 (2021-11-04)
* Update dependencies | [64631a4](https://github.com/undergroundwires/privacy.sexy/commit/64631a4552fad7f7b06286aba8d3ca2d731f9342)

111
README.md
View File

@@ -2,104 +2,21 @@
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
<!-- markdownlint-disable MD033 -->
<p align="center">
<a href="https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md">
<img
alt="contributions are welcome"
src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat"
/>
</a>
<!-- Code quality -->
<br />
<a href="https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript">
<img
alt="Language grade: JavaScript/TypeScript"
src="https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18"
/>
</a>
<a href="https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability">
<img
alt="Maintainability"
src="https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability"
/>
</a>
<!-- Tests -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.unit.yaml">
<img
alt="Unit tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/unit-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.integration.yaml">
<img
alt="Integration tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/integration-tests/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/tests.e2e.yaml">
<img
alt="E2E tests status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/e2e-tests/badge.svg"
/>
</a>
<!-- Checks -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml">
<img
alt="Quality checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.security.yaml">
<img
alt="Security checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/security-checks/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml">
<img
alt="Build checks status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/build-checks/badge.svg"
/>
</a>
<!-- Release -->
<br />
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.git.yaml">
<img
alt="Git release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-git/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.site.yaml">
<img
alt="Site release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-site/badge.svg"
/>
</a>
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/release.desktop.yaml">
<img
alt="Desktop application release status"
src="https://github.com/undergroundwires/privacy.sexy/workflows/release-desktop/badge.svg"
/>
</a>
<!-- Others -->
<br />
<a href="https://github.com/undergroundwires/bump-everywhere">
<img
alt="Auto-versioned by bump-everywhere"
src="https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true"
/>
</a>
</p>
<!-- markdownlint-restore -->
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
[![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
[![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Quality checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Quality%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Security checks status](https://github.com/undergroundwires/privacy.sexy/workflows/Security%20checks/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Bump & release status](https://github.com/undergroundwires/privacy.sexy/workflows/Bump%20&%20release/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
## Get started
- Online version at [https://privacy.sexy](https://privacy.sexy)
- 💡 No need to run any compiled software on your computer.
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-Setup-0.11.2.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.2/privacy.sexy-0.11.2.AppImage).
- Alternatively download offline version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-Setup-0.11.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.dmg) or [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.11.1/privacy.sexy-0.11.1.AppImage).
- 💡 Single click to execute your script.
- ❗ Come back regularly to apply latest version for stronger privacy and security.
@@ -132,9 +49,6 @@
- Testing
- Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:integration`
- Run e2e (end-to-end) tests
- Interactive mode with GUI: `npm run test:e2e`
- Headless mode without GUI: `npm run test:e2e -- --headless`
- Lint: `npm run lint`
- **Desktop app**
- Development: `npm run electron:serve`
@@ -143,8 +57,8 @@
- Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.2 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.2 undergroundwires/privacy.sexy:0.11.2`
1. Build: `docker build -t undergroundwires/privacy.sexy:0.11.1 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.11.1 undergroundwires/privacy.sexy:0.11.1`
## Architecture overview
@@ -170,6 +84,5 @@
- CI/CD is fully automated for this repo using different GIT events & GitHub actions.
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
- Everything that's merged in the master goes directly to production.
- 📖 Read more on [CI/CD pipelines](./docs/ci-cd.md)
[![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/)

View File

@@ -1,5 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -1,3 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

View File

@@ -1,19 +0,0 @@
# Pipelines
Pipelines are found under [`.github/workflows`](./../.github/workflows).
## Pipeline types
They are categorized based on their type:
- `tests`: Different types of tests to verify functionality.
- `checks`: Other controls such as vulnerability scans or styling checks.
- `release`: Pipelines used for release of deployment such as building and testing.
## Naming conventions
Pipeline files are named using: **`<type>.<name>.yaml`**.
**`type`**: Sub-folders do not work for GitHub workflows so that's why `<type>.` prefix is used. See also [pipeline types](#pipeline-types).
**`name`**: Pipeline themselves are named using kebab case. It allows for easier URL references for their status badges. E.g. file name `tests.unit.yaml`, pipeline name: `name: unit-tests`

View File

@@ -3,59 +3,41 @@
- There are two different types of tests executed:
1. [Unit tests](#unit-tests)
2. [Integration tests](#integration-tests)
3. [End-to-end (E2E) tests](#e2e-tests)
- All tests
- Uses [Mocha](https://mochajs.org/) and [Chai](https://www.chaijs.com/).
- Are written in files that includes `.spec` extension.
- 💡 You can use path/module alias `@/tests` in import statements.
## Unit tests
- Tests each component in isolation.
- Defined in [`./tests/unit`](./../tests/unit).
- They follow same folder structure as [`./src`](./../src).
- Tests each component in isolation
- Defined in [`./tests/unit`](./../tests/unit)
- They follow same folder structure as [`./src`](./../src)
### Naming
- Each test suite first describe the system under test.
- E.g. tests for class `Application` is categorized under `Application`.
- Tests for specific methods are categorized under method name (if applicable).
- E.g. test for `run()` is categorized under `run`.
- Each test suite first describe the system under test
- E.g. tests for class `Application` is categorized under `Application`
- Tests for specific methods are categorized under method name (if applicable)
- E.g. test for `run()` is categorized under `run`
### Act, arrange, assert
- Tests use act, arrange and assert (AAA) pattern when applicable.
- Tests use act, arrange and assert (AAA) pattern when applicable
- **Arrange**
- Should set up the test case.
- Starts with comment line `// arrange`.
- Should set up the test case
- Starts with comment line `// arrange`
- **Act**
- Should cover the main thing to be tested.
- Starts with comment line `// act`.
- Should cover the main thing to be tested
- Starts with comment line `// act`
- **Assert**
- Should elicit some sort of response.
- Starts with comment line `// assert`.
- Should elicit some sort of response
- Starts with comment line `// assert`
### Stubs
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs).
- They implement dummy behavior to be functional.
- Stubs are defined in [`./tests/stubs`](./../tests/unit/stubs)
- They implement dummy behavior to be functional
## Integration tests
- Tests functionality of a component in combination with others (not isolated).
- Ensure dependencies to third parties work as expected.
- Defined in [`./tests/integration`](./../tests/integration).
## E2E tests
- Test the functionality and performance of a running application.
- E2E tests are configured by vue plugin [`e2e-cypress`](https://github.com/vuejs/vue-cli/tree/dev/packages/@vue/cli-plugin-e2e-cypress#readme) for Vue CLI.
- Names and folders are structured logically based on tests.
- The structure is following:
- [`cypress.json`](./../cypress.json): Cypress configuration file.
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder.
- [`/specs/`](./../tests/e2e/specs/): Test files, test are named with `.spec.js` extension.
- [`/plugins/index.js`](./../tests/e2e/plugins/index.js): Plugin file executed before project is loaded.
- [`/support/index.js`](./../tests/e2e/support/index.js): Support file, runs before every single spec file.
- *(Ignored)* `/videos`: Asset folder for videos taken during tests.
- *(Ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
- Tests functionality of a component in combination with others (not isolated)
- Ensure dependencies to third parties work as expected
- Defined in [`./tests/integration`](./../tests/integration)

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 579 KiB

36883
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "privacy.sexy",
"version": "0.11.2",
"version": "0.11.1",
"private": true,
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
"author": "undergroundwires",
@@ -8,18 +8,17 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\"",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:eslint": "vue-cli-service lint --no-fix --mode production",
"lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"test:integration": "vue-cli-service test:unit \"tests/integration/**/*.spec.ts\""
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
@@ -48,27 +47,16 @@
"@types/chai": "^4.2.22",
"@types/file-saver": "^2.0.3",
"@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0-rc.1",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-rc.1",
"@vue/cli-plugin-eslint": "~5.0.0-rc.1",
"@vue/cli-plugin-typescript": "~5.0.0-rc.1",
"@vue/cli-plugin-unit-mocha": "~5.0.0-rc.1",
"@vue/cli-service": "~5.0.0-rc.1",
"@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/cli-plugin-babel": "^4.5.14",
"@vue/cli-plugin-typescript": "^4.5.14",
"@vue/cli-plugin-unit-mocha": "^4.5.14",
"@vue/cli-service": "^4.5.14",
"@vue/test-utils": "1.2.2",
"chai": "^4.3.4",
"cypress": "^8.3.0",
"electron": "^15.3.0",
"electron-devtools-installer": "^3.2.0",
"electron-log": "^4.4.1",
"electron-updater": "^4.3.9",
"eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-vue": "^8.0.3",
"eslint-plugin-vuejs-accessibility": "^1.1.0",
"js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.29.0",
"raw-loader": "^4.0.2",
@@ -78,16 +66,12 @@
"remark-validate-links": "^11.0.1",
"sass": "^1.43.3",
"sass-loader": "10.2.0",
"ts-loader": "9.0.1",
"tslib": "^2.3.1",
"typescript": "^4.4.4",
"vue-cli-plugin-electron-builder": "^2.1.1",
"vue-template-compiler": "^2.6.14",
"yaml-lint": "^1.2.4"
},
"//devDependencies": {
"ts-loader": "Here as workaround for vue-cli-plugin-electron-builder using older webpack 4"
},
"homepage": "https://privacy.sexy",
"repository": {
"type": "git",

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};
autoprefixer: {}
}
}

View File

@@ -7,18 +7,15 @@ export type ApplicationGetter = () => IApplication;
const ApplicationGetter: ApplicationGetter = parseApplication;
export class ApplicationFactory implements IApplicationFactory {
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) {
throw new Error('undefined getter');
public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter);
private readonly getter: AsyncLazy<IApplication>;
protected constructor(costlyGetter: ApplicationGetter) {
if (!costlyGetter) {
throw new Error('undefined getter');
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
}
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
}
public getApp(): Promise<IApplication> {
return this.getter.getValue();
}
}

View File

@@ -1,21 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) {
return array.slice().sort();
}
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) {
return array.slice().sort();
}
}
// Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) {
return false;
}
return array1.every((val, index) => val === array2[index]);
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) {
return false;
}
return array1.every((val, index) => val === array2[index]);
}

View File

@@ -1,63 +1,54 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
= { [key in T]: TEnumValue };
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum;
parseEnum(value: string, propertyName: string): TEnum;
}
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>,
): IEnumParser<TEnumValue> {
return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
};
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
};
}
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
value: string,
enumName: string,
enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue {
if (!value) {
throw new Error(`undefined ${enumName}`);
}
if (typeof value !== 'string') {
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
}
const casedValue = getEnumNames(enumVariable)
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
if (!casedValue) {
throw new Error(`unknown ${enumName}: "${value}"`);
}
return enumVariable[casedValue as keyof typeof enumVariable];
value: string,
enumName: string,
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
if (!value) {
throw new Error(`undefined ${enumName}`);
}
if (typeof value !== 'string') {
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
}
const casedValue = getEnumNames(enumVariable)
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
if (!casedValue) {
throw new Error(`unknown ${enumName}: "${value}"`);
}
return enumVariable[casedValue as keyof typeof enumVariable];
}
export function getEnumNames
<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>,
): string[] {
return Object
.values(enumVariable)
.filter((enumMember) => typeof enumMember === 'string') as string[];
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>): string[] {
return Object
.values(enumVariable)
.filter((enumMember) => typeof enumMember === 'string') as string[];
}
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>,
): TEnumValue[] {
return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[];
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[];
}
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>,
) {
if (value === undefined) {
throw new Error('undefined enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) {
if (value === undefined) {
throw new Error('undefined enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}
}

View File

@@ -1,5 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T;
create(language: ScriptingLanguage): T;
}

View File

@@ -1,30 +1,31 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { assertInRange } from '@/application/Common/Enum';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('undefined getter');
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
}

View File

@@ -1,64 +1,60 @@
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();
public collection: ICategoryCollection;
public currentOs: OperatingSystem;
public collection: ICategoryCollection;
public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
}
private readonly states: StateMachine;
public constructor(
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
validateApp(app);
assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
if (this.currentOs === os) {
return;
public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
private readonly states: StateMachine;
public constructor(
public readonly app: IApplication,
initialContext: OperatingSystem) {
validateApp(app);
assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
if (this.currentOs === os) {
return;
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
};
this.contextChanged.notify(event);
this.currentOs = os;
}
const event: IApplicationContextChangedEvent = {
newState: this.states[os],
oldState: this.states[this.currentOs],
};
this.contextChanged.notify(event);
this.currentOs = os;
}
}
function validateApp(app: IApplication) {
if (!app) {
throw new Error('undefined app');
}
if (!app) {
throw new Error('undefined app');
}
}
function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection);
}
return machine;
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection);
}
return machine;
}

View File

@@ -1,32 +1,31 @@
import { ApplicationContext } from './ApplicationContext';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IApplication } from '@/domain/IApplication';
import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication';
import { IEnvironment } from '../Environment/IEnvironment';
import { IApplicationFactory } from '../IApplicationFactory';
import { ApplicationFactory } from '../ApplicationFactory';
import { ApplicationContext } from './ApplicationContext';
export async function buildContext(
factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment,
): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getApp();
const os = getInitialOs(app, environment);
return new ApplicationContext(app, os);
factory: IApplicationFactory = ApplicationFactory.Current,
environment = Environment.CurrentEnvironment): Promise<IApplicationContext> {
if (!factory) { throw new Error('undefined factory'); }
if (!environment) { throw new Error('undefined environment'); }
const app = await factory.getApp();
const os = getInitialOs(app, environment);
return new ApplicationContext(app, os);
}
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
const currentOs = environment.os;
const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) {
return currentOs;
}
supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1);
});
return supportedOsList[0];
const currentOs = environment.os;
const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) {
return currentOs;
}
supportedOsList.sort((os1, os2) => {
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
return getPriority(os2) - getPriority(os1);
});
return supportedOsList[0];
}

View File

@@ -1,20 +1,16 @@
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IApplication } from '@/domain/IApplication';
import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState';
export interface IReadOnlyApplicationContext {
readonly app: IApplication;
readonly state: IReadOnlyCategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
}
export interface IApplicationContext extends IReadOnlyApplicationContext {
export interface IApplicationContext {
readonly app: IApplication;
readonly state: ICategoryCollectionState;
readonly contextChanged: IEventSource<IApplicationContextChangedEvent>;
changeContext(os: OperatingSystem): void;
}
export interface IApplicationContextChangedEvent {
readonly newState: ICategoryCollectionState;
readonly oldState: ICategoryCollectionState;
readonly newState: ICategoryCollectionState;
readonly oldState: ICategoryCollectionState;
}

View File

@@ -1,5 +1,3 @@
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { UserFilter } from './Filter/UserFilter';
import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode';
@@ -7,20 +5,19 @@ import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection';
import { ICategoryCollectionState } from './ICategoryCollectionState';
import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class CategoryCollectionState implements ICategoryCollectionState {
public readonly os: OperatingSystem;
public readonly os: OperatingSystem;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly filter: IUserFilter;
public readonly code: IApplicationCode;
public readonly selection: IUserSelection;
public readonly filter: IUserFilter;
public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
this.os = collection.os;
}
public constructor(readonly collection: ICategoryCollection) {
this.selection = new UserSelection(collection, []);
this.code = new ApplicationCode(this.selection, collection.scripting);
this.filter = new UserFilter(collection);
this.os = collection.os;
}
}

View File

@@ -1,41 +1,39 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export class ApplicationCode implements IApplicationCode {
public readonly changed = new EventSource<ICodeChangedEvent>();
public readonly changed = new EventSource<ICodeChangedEvent>();
public current: string;
public current: string;
private scriptPositions = new Map<SelectedScript, CodePosition>();
private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor(
userSelection: IUserSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
if (!generator) { throw new Error('generator is null or undefined'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}
constructor(
userSelection: IReadOnlyUserSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
if (!generator) { throw new Error('generator is null or undefined'); }
this.setCode(userSelection.selectedScripts);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
this.changed.notify(event);
}
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
this.changed.notify(event);
}
}

View File

@@ -1,71 +1,64 @@
import { ICodeChangedEvent } from './ICodeChangedEvent';
import { SelectedScript } from '../../Selection/SelectedScript';
import { IScript } from '@/domain/IScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { SelectedScript } from '../../Selection/SelectedScript';
import { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string;
public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>;
public readonly removedScripts: ReadonlyArray<IScript>;
public readonly changedScripts: ReadonlyArray<IScript>;
public readonly addedScripts: ReadonlyArray<IScript>;
private readonly scripts: Map<IScript, ICodePosition>;
public readonly removedScripts: ReadonlyArray<IScript>;
constructor(
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}
public readonly changedScripts: ReadonlyArray<IScript>;
public isEmpty(): boolean {
return this.scripts.size === 0;
}
private readonly scripts: Map<IScript, ICodePosition>;
constructor(
code: string,
oldScripts: ReadonlyArray<SelectedScript>,
scripts: Map<SelectedScript, ICodePosition>,
) {
ensureAllPositionsExist(code, Array.from(scripts.values()));
this.code = code;
const newScripts = Array.from(scripts.keys());
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
}
public isEmpty(): boolean {
return this.scripts.size === 0;
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
}
public getScriptPositionInCode(script: IScript): ICodePosition {
return this.scripts.get(script);
}
}
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length;
const missingPositions = positions.filter((position) => position.endLine > totalLines);
if (missingPositions.length > 0) {
throw new Error(
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
+ `(total code lines: ${totalLines}).`,
);
}
const totalLines = script.split(/\r\n|\r|\n/).length;
for (const position of positions) {
if (position.endLine > totalLines) {
throw new Error(`script end line (${position.endLine}) is out of range.` +
`(total code lines: ${totalLines}`);
}
}
}
function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>,
): ReadonlyArray<IScript> {
return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert))
.map((selection) => selection.script);
oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert ))
.map((selection) => selection.script);
}
function selectIfNotExists(
selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>,
) {
return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script);
selectableContainer: ReadonlyArray<SelectedScript>,
test: ReadonlyArray<SelectedScript>) {
return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.map((selection) => selection.script);
}

View File

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

View File

@@ -4,65 +4,64 @@ const NewLine = '\n';
const TotalFunctionSeparatorChars = 58;
export abstract class CodeBuilder implements ICodeBuilder {
private readonly lines = new Array<string>();
private readonly lines = new Array<string>();
// Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number {
return this.lines.length;
}
public appendLine(code?: string): CodeBuilder {
if (!code) {
this.lines.push('');
return this;
// Returns current line starting from 0 (no lines), or 1 (have single line)
public get currentLine(): number {
return this.lines.length;
}
const lines = code.match(/[^\r\n]+/g);
this.lines.push(...lines);
return this;
}
public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
}
public appendCommentLine(commentLine?: string): CodeBuilder {
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
return this;
}
public appendFunction(name: string, code: string): CodeBuilder {
if (!name) { throw new Error('name cannot be empty or null'); }
if (!code) { throw new Error('code cannot be empty or null'); }
return this
.appendCommentLineWithHyphensAround(name)
.appendLine(this.writeStandardOut(`--- ${name}`))
.appendLine(code)
.appendTrailingHyphensCommentLine();
}
public appendCommentLineWithHyphensAround(
sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars,
): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName);
public appendLine(code?: string): CodeBuilder {
if (!code) {
this.lines.push('');
return this;
}
const lines = code.match(/[^\r\n]+/g);
for (const line of lines) {
this.lines.push(line);
}
return this;
}
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
return this
.appendTrailingHyphensCommentLine()
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
}
public toString(): string {
return this.lines.join(NewLine);
}
public appendTrailingHyphensCommentLine(
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
return this.appendCommentLine('-'.repeat(totalRepeatHyphens));
}
protected abstract getCommentDelimiter(): string;
public appendCommentLine(commentLine?: string): CodeBuilder {
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
return this;
}
protected abstract writeStandardOut(text: string): string;
public appendFunction(name: string, code: string): CodeBuilder {
if (!name) { throw new Error('name cannot be empty or null'); }
if (!code) { throw new Error('code cannot be empty or null'); }
return this
.appendCommentLineWithHyphensAround(name)
.appendLine(this.writeStandardOut(`--- ${name}`))
.appendLine(code)
.appendTrailingHyphensCommentLine();
}
public appendCommentLineWithHyphensAround(
sectionName: string,
totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder {
if (!sectionName) { throw new Error('sectionName cannot be empty or null'); }
if (sectionName.length >= totalRepeatHyphens) {
return this.appendCommentLine(sectionName);
}
const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2));
const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2));
return this
.appendTrailingHyphensCommentLine()
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
}
public toString(): string {
return this.lines.join(NewLine);
}
protected abstract getCommentDelimiter(): string;
protected abstract writeStandardOut(text: string): string;
}

View File

@@ -5,12 +5,10 @@ import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory
extends ScriptingLanguageFactory<ICodeBuilder>
implements ICodeBuilderFactory {
constructor() {
super();
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
}
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
constructor() {
super();
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
}
}

View File

@@ -1,9 +1,9 @@
export interface ICodeBuilder {
currentLine: number;
appendLine(code?: string): ICodeBuilder;
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
appendCommentLine(commentLine?: string): ICodeBuilder;
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
appendFunction(name: string, code: string): ICodeBuilder;
toString(): string;
currentLine: number;
appendLine(code?: string): ICodeBuilder;
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
appendCommentLine(commentLine?: string): ICodeBuilder;
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
appendFunction(name: string, code: string): ICodeBuilder;
toString(): string;
}

View File

@@ -1,4 +1,5 @@
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export type ICodeBuilderFactory = IScriptingLanguageFactory<ICodeBuilder>;
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
}

View File

@@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface IUserScript {
code: string;
scriptPositions: Map<SelectedScript, ICodePosition>;
code: string;
scriptPositions: Map<SelectedScript, ICodePosition>;
}

View File

@@ -1,9 +1,9 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
export interface IUserScriptGenerator {
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript;
buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript;
}

View File

@@ -1,17 +1,16 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class BatchBuilder extends CodeBuilder {
protected getCommentDelimiter(): string {
return '::';
}
protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`;
}
protected getCommentDelimiter(): string {
return '::';
}
protected writeStandardOut(text: string): string {
return `echo ${escapeForEcho(text)}`;
}
}
function escapeForEcho(text: string) {
return text
.replace(/&/g, '^&')
.replace(/%/g, '%%');
return text
.replace(/&/g, '^&')
.replace(/%/g, '%%');
}

View File

@@ -1,16 +1,15 @@
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
export class ShellBuilder extends CodeBuilder {
protected getCommentDelimiter(): string {
return '#';
}
protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`;
}
protected getCommentDelimiter(): string {
return '#';
}
protected writeStandardOut(text: string): string {
return `echo '${escapeForEcho(text)}'`;
}
}
function escapeForEcho(text: string) {
return text
.replace(/'/g, '\'\\\'\'');
return text
.replace(/'/g, '\'\\\'\'');
}

View File

@@ -1,75 +1,71 @@
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import { CodePosition } from '../Position/CodePosition';
import { IUserScript } from './IUserScript';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { CodeBuilderFactory } from './CodeBuilderFactory';
export class UserScriptGenerator implements IUserScriptGenerator {
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
}
public buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition,
): IUserScript {
if (!selectedScripts) { throw new Error('undefined scripts'); }
if (!scriptingDefinition) { throw new Error('undefined definition'); }
if (!selectedScripts.length) {
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
}
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder);
const scriptPositions = selectedScripts.reduce((result, selection) => {
return appendSelection(selection, result, builder);
}, new Map<SelectedScript, ICodePosition>());
const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions };
}
public buildCode(
selectedScripts: ReadonlyArray<SelectedScript>,
scriptingDefinition: IScriptingDefinition): IUserScript {
if (!selectedScripts) { throw new Error('undefined scripts'); }
if (!scriptingDefinition) { throw new Error('undefined definition'); }
let scriptPositions = new Map<SelectedScript, ICodePosition>();
if (!selectedScripts.length) {
return { code: '', scriptPositions };
}
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
builder = initializeCode(scriptingDefinition.startCode, builder);
for (const selection of selectedScripts) {
scriptPositions = appendSelection(selection, scriptPositions, builder);
}
const code = finalizeCode(builder, scriptingDefinition.endCode);
return { code, scriptPositions };
}
}
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
if (!startCode) {
return builder;
}
return builder
.appendLine(startCode)
.appendLine();
if (!startCode) {
return builder;
}
return builder
.appendLine(startCode)
.appendLine();
}
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
if (!endCode) {
return builder.toString();
}
return builder.appendLine()
.appendLine(endCode)
.toString();
if (!endCode) {
return builder.toString();
}
return builder.appendLine()
.appendLine(endCode)
.toString();
}
function appendSelection(
selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>,
builder: ICodeBuilder,
): Map<SelectedScript, ICodePosition> {
// Start from next line because first line will be empty to separate scripts
const startPosition = builder.currentLine + 1;
appendCode(selection, builder);
const endPosition = builder.currentLine - 1;
builder.appendLine();
const position = new CodePosition(startPosition, endPosition);
scriptPositions.set(selection, position);
return scriptPositions;
selection: SelectedScript,
scriptPositions: Map<SelectedScript, ICodePosition>,
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
builder = appendCode(selection, builder);
const endPosition = builder.currentLine - 1;
builder.appendLine();
const position = new CodePosition(startPosition, endPosition);
scriptPositions.set(selection, position);
return scriptPositions;
}
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
const { script } = selection;
const name = selection.revert ? `${script.name} (revert)` : script.name;
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
return builder
.appendLine()
.appendFunction(name, scriptCode);
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
return builder
.appendLine()
.appendFunction(name, scriptCode);
}

View File

@@ -1,7 +1,7 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
export interface IApplicationCode {
readonly changed: IEventSource<ICodeChangedEvent>;
readonly current: string;
readonly changed: IEventSource<ICodeChangedEvent>;
readonly current: string;
}

View File

@@ -1,25 +1,24 @@
import { ICodePosition } from './ICodePosition';
export class CodePosition implements ICodePosition {
public get totalLines(): number {
return this.endLine - this.startLine;
}
public get totalLines(): number {
return this.endLine - this.startLine;
}
constructor(
public readonly startLine: number,
public readonly endLine: number,
) {
if (startLine < 0) {
throw new Error('Code cannot start in a negative line');
constructor(
public readonly startLine: number,
public readonly endLine: number) {
if (startLine < 0) {
throw new Error('Code cannot start in a negative line');
}
if (endLine < 0) {
throw new Error('Code cannot end in a negative line');
}
if (endLine === startLine) {
throw new Error('Empty code');
}
if (endLine < startLine) {
throw new Error('End line cannot be less than start line');
}
}
if (endLine < 0) {
throw new Error('Code cannot end in a negative line');
}
if (endLine === startLine) {
throw new Error('Empty code');
}
if (endLine < startLine) {
throw new Error('End line cannot be less than start line');
}
}
}

View File

@@ -1,5 +1,5 @@
export interface ICodePosition {
readonly startLine: number;
readonly endLine: number;
readonly totalLines: number;
readonly startLine: number;
readonly endLine: number;
readonly totalLines: number;
}

View File

@@ -1,20 +1,18 @@
import { IFilterResult } from './IFilterResult';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IFilterResult } from './IFilterResult';
export class FilterResult implements IFilterResult {
constructor(
public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {
return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0;
}
constructor(
public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly query: string) {
if (!query) { throw new Error('Query is empty or undefined'); }
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
}
public hasAnyMatches(): boolean {
return this.scriptMatches.length > 0
|| this.categoryMatches.length > 0;
}
}

View File

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

View File

@@ -1,13 +1,10 @@
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { IFilterResult } from './IFilterResult';
export interface IReadOnlyUserFilter {
readonly currentFilter: IFilterResult | undefined;
readonly filtered: IEventSource<IFilterResult>;
readonly filterRemoved: IEventSource<void>;
}
export interface IUserFilter extends IReadOnlyUserFilter {
setFilter(filter: string): void;
removeFilter(): void;
export interface IUserFilter {
readonly currentFilter: IFilterResult | undefined;
readonly filtered: IEventSource<IFilterResult>;
readonly filterRemoved: IEventSource<void>;
setFilter(filter: string): void;
removeFilter(): void;
}

View File

@@ -1,56 +1,52 @@
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult';
import { IUserFilter } from './IUserFilter';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
export class UserFilter implements IUserFilter {
public readonly filtered = new EventSource<IFilterResult>();
public readonly filtered = new EventSource<IFilterResult>();
public readonly filterRemoved = new EventSource<void>();
public currentFilter: IFilterResult | undefined;
public readonly filterRemoved = new EventSource<void>();
constructor(private collection: ICategoryCollection) {
public currentFilter: IFilterResult | undefined;
constructor(private collection: ICategoryCollection) {
}
public setFilter(filter: string): void {
if (!filter) {
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
}
const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.collection.getAllScripts().filter(
(script) => isScriptAMatch(script, filterLowercase),
);
const filteredCategories = this.collection.getAllCategories().filter(
(category) => category.name.toLowerCase().includes(filterLowercase),
);
const matches = new FilterResult(
filteredScripts,
filteredCategories,
filter,
);
this.currentFilter = matches;
this.filtered.notify(matches);
}
public removeFilter(): void {
this.currentFilter = undefined;
this.filterRemoved.notify();
}
public setFilter(filter: string): void {
if (!filter) {
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
}
const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.collection.getAllScripts().filter(
(script) => isScriptAMatch(script, filterLowercase));
const filteredCategories = this.collection.getAllCategories().filter(
(category) => category.name.toLowerCase().includes(filterLowercase));
const matches = new FilterResult(
filteredScripts,
filteredCategories,
filter,
);
this.currentFilter = matches;
this.filtered.notify(matches);
}
public removeFilter(): void {
this.currentFilter = undefined;
this.filterRemoved.notify();
}
}
function isScriptAMatch(script: IScript, filterLowercase: string) {
if (script.name.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.code.revert) {
return script.code.revert.toLowerCase().includes(filterLowercase);
}
return false;
if (script.name.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.code.revert) {
return script.code.revert.toLowerCase().includes(filterLowercase);
}
return false;
}

View File

@@ -1,18 +1,13 @@
import { IUserFilter } from './Filter/IUserFilter';
import { IUserSelection } from './Selection/IUserSelection';
import { IApplicationCode } from './Code/IApplicationCode';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
import { IApplicationCode } from './Code/IApplicationCode';
export interface IReadOnlyCategoryCollectionState {
readonly code: IApplicationCode;
readonly os: OperatingSystem;
readonly filter: IReadOnlyUserFilter;
readonly selection: IReadOnlyUserSelection;
readonly collection: ICategoryCollection;
}
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
readonly filter: IUserFilter;
readonly selection: IUserSelection;
export interface ICategoryCollectionState {
readonly code: IApplicationCode;
readonly filter: IUserFilter;
readonly selection: IUserSelection;
readonly collection: ICategoryCollection;
readonly os: OperatingSystem;
}

View File

@@ -1,23 +1,20 @@
import { SelectedScript } from './SelectedScript';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IEventSource } from '@/infrastructure/Events/IEventSource';
import { SelectedScript } from './SelectedScript';
export interface IReadOnlyUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
isSelected(scriptId: string): boolean;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
}
export interface IUserSelection extends IReadOnlyUserSelection {
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
selectAll(): void;
deselectAll(): void;
export interface IUserSelection {
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
isSelected(scriptId: string): boolean;
selectAll(): void;
deselectAll(): void;
}

View File

@@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript';
export class SelectedScript extends BaseEntity<string> {
constructor(
public readonly script: IScript,
public readonly revert: boolean,
) {
super(script.id);
if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script');
constructor(
public readonly script: IScript,
public readonly revert: boolean,
) {
super(script.id);
if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script');
}
}
}
}

View File

@@ -1,146 +1,141 @@
import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { IUserSelection } from './IUserSelection';
import { SelectedScript } from './SelectedScript';
export class UserSelection implements IUserSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript>;
private readonly scripts: IRepository<string, SelectedScript>;
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) {
this.scripts.addItem(script);
}
}
}
constructor(
private readonly collection: ICategoryCollection,
selectedScripts: ReadonlyArray<SelectedScript>,
) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
for (const script of selectedScripts) {
this.scripts.addItem(script);
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
}
}
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every(
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
);
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): void {
const category = this.collection.findCategory(categoryId);
const scriptsToRemove = category.getAllScriptsRecursively()
.filter((script) => this.scripts.exists(script.id));
if (!scriptsToRemove.length) {
return;
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
const category = this.collection.findCategory(categoryId);
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
.filter((script) =>
!this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
);
if (!scriptsToAddOrUpdate.length) {
return;
}
for (const script of scriptsToAddOrUpdate) {
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
}
this.changed.notify(this.scripts.getItems());
}
for (const script of scriptsToRemove) {
this.scripts.removeItem(script.id);
}
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
const scriptsToAddOrUpdate = this.collection
.findCategory(categoryId)
.getAllScriptsRecursively()
.filter(
(script) => !this.scripts.exists(script.id)
|| this.scripts.getById(script.id).revert !== revert,
)
.map((script) => new SelectedScript(script, revert));
if (!scriptsToAddOrUpdate.length) {
return;
public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
}
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
for (const script of scriptsToAddOrUpdate) {
this.scripts.addOrUpdateItem(script);
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
this.changed.notify(this.scripts.getItems());
}
public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems());
}
const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.collection.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems());
}
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
/** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems();
}
public selectAll(): void {
const scriptsToSelect = this.collection
.getAllScripts()
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const script of scriptsToSelect) {
this.scripts.addItem(script);
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
this.changed.notify(this.scripts.getItems());
}
public deselectAll(): void {
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
for (const scriptId of selectedScriptIds) {
this.scripts.removeItem(scriptId);
/** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems();
}
this.changed.notify([]);
}
public selectOnly(scripts: readonly IScript[]): void {
if (!scripts || scripts.length === 0) {
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
public selectAll(): void {
for (const script of this.collection.getAllScripts()) {
if (!this.scripts.exists(script.id)) {
const selection = new SelectedScript(script, false);
this.scripts.addItem(selection);
}
}
this.changed.notify(this.scripts.getItems());
}
// Unselect from selected scripts
if (this.scripts.length !== 0) {
this.scripts.getItems()
.filter((existing) => !scripts.some((script) => existing.id === script.id))
.map((script) => script.id)
.forEach((scriptId) => this.scripts.removeItem(scriptId));
public deselectAll(): void {
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
for (const scriptId of selectedScriptIds) {
this.scripts.removeItem(scriptId);
}
this.changed.notify([]);
}
// Select from unselected scripts
const unselectedScripts = scripts
.filter((script) => !this.scripts.exists(script.id))
.map((script) => new SelectedScript(script, false));
for (const toSelect of unselectedScripts) {
this.scripts.addItem(toSelect);
public selectOnly(scripts: readonly IScript[]): void {
if (!scripts || scripts.length === 0) {
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
}
// Unselect from selected scripts
if (this.scripts.length !== 0) {
this.scripts.getItems()
.filter((existing) => !scripts.some((script) => existing.id === script.id))
.map((script) => script.id)
.forEach((scriptId) => this.scripts.removeItem(scriptId));
}
// Select from unselected scripts
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
for (const toSelect of unselectedScripts) {
const selection = new SelectedScript(toSelect, false);
this.scripts.addItem(selection);
}
this.changed.notify(this.scripts.getItems());
}
this.changed.notify(this.scripts.getItems());
}
}

View File

@@ -3,55 +3,52 @@ import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) {
return undefined;
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) {
return undefined;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== undefined) {
return os;
}
}
return undefined;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== undefined) {
return os;
}
}
return undefined;
}
}
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors = [
define(OperatingSystem.KaiOS, (b) => b
.mustInclude('KAIOS')),
define(OperatingSystem.ChromeOS, (b) => b
.mustInclude('CrOS')),
define(OperatingSystem.BlackBerryOS, (b) => b
.mustInclude('BlackBerry')),
define(OperatingSystem.BlackBerryTabletOS, (b) => b
.mustInclude('RIM Tablet OS')),
define(OperatingSystem.BlackBerry, (b) => b
.mustInclude('BB10')),
define(OperatingSystem.Android, (b) => b
.mustInclude('Android').mustNotInclude('Windows Phone')),
define(OperatingSystem.Android, (b) => b
.mustInclude('Adr').mustNotInclude('Windows Phone')),
define(OperatingSystem.iOS, (b) => b
.mustInclude('like Mac OS X')),
define(OperatingSystem.Linux, (b) => b
.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
define(OperatingSystem.Windows, (b) => b
.mustInclude('Windows').mustNotInclude('Windows Phone')),
define(OperatingSystem.WindowsPhone, (b) => b
.mustInclude('Windows Phone')),
define(OperatingSystem.macOS, (b) => b
.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
const BrowserDetectors =
[
define(OperatingSystem.KaiOS, (b) =>
b.mustInclude('KAIOS')),
define(OperatingSystem.ChromeOS, (b) =>
b.mustInclude('CrOS')),
define(OperatingSystem.BlackBerryOS, (b) =>
b.mustInclude('BlackBerry')),
define(OperatingSystem.BlackBerryTabletOS, (b) =>
b.mustInclude('RIM Tablet OS')),
define(OperatingSystem.BlackBerry, (b) =>
b.mustInclude('BB10')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Android').mustNotInclude('Windows Phone')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
define(OperatingSystem.iOS, (b) =>
b.mustInclude('like Mac OS X')),
define(OperatingSystem.Linux, (b) =>
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
define(OperatingSystem.Windows, (b) =>
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
define(OperatingSystem.WindowsPhone, (b) =>
b.mustInclude('Windows Phone')),
define(OperatingSystem.macOS, (b) =>
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
];
function define(
os: OperatingSystem,
applyRules: (builder: DetectorBuilder) => DetectorBuilder,
): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
}

View File

@@ -1,54 +1,53 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>();
private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { }
constructor(private readonly os: OperatingSystem) { }
public mustInclude(str: string): DetectorBuilder {
return this.add(str, this.existingPartsInUserAgent);
}
public mustInclude(str: string): DetectorBuilder {
return this.add(str, this.existingPartsInUserAgent);
}
public mustNotInclude(str: string): DetectorBuilder {
return this.add(str, this.notExistingPartsInUserAgent);
}
public mustNotInclude(str: string): DetectorBuilder {
return this.add(str, this.notExistingPartsInUserAgent);
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
}
return {
detect: (agent) => this.detect(agent),
};
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
private detect(userAgent: string): OperatingSystem {
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return undefined;
}
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return undefined;
}
return this.os;
}
return {
detect: (agent) => this.detect(agent),
};
}
private detect(userAgent: string): OperatingSystem {
if (!userAgent) {
throw new Error('User agent is null or undefined');
private add(part: string, array: string[]): DetectorBuilder {
if (!part) {
throw new Error('part is empty or undefined');
}
if (this.existingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as existing part`);
}
if (this.notExistingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as not existing part`);
}
array.push(part);
return this;
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return undefined;
}
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return undefined;
}
return this.os;
}
private add(part: string, array: string[]): DetectorBuilder {
if (!part) {
throw new Error('part is empty or undefined');
}
if (this.existingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as existing part`);
}
if (this.notExistingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as not existing part`);
}
array.push(part);
return this;
}
}

View File

@@ -1,5 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem | undefined;
detect(userAgent: string): OperatingSystem | undefined;
}

View File

@@ -1,89 +1,80 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironmentVariables {
readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process;
readonly navigator: Navigator;
interface IEnvironmentVariables {
readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process;
readonly navigator: Navigator;
}
export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({
window,
process: typeof process !== 'undefined' ? process /* electron only */ : undefined,
navigator,
});
public readonly isDesktop: boolean;
public readonly os: OperatingSystem;
protected constructor(
variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector(),
) {
if (!variables) {
throw new Error('variables is null or empty');
public static readonly CurrentEnvironment: IEnvironment = new Environment({
window,
process,
navigator,
});
public readonly isDesktop: boolean;
public readonly os: OperatingSystem;
protected constructor(
variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
if (!variables) {
throw new Error('variables is null or empty');
}
this.isDesktop = isDesktop(variables);
this.os = this.isDesktop ?
getDesktopOsType(getProcessPlatform(variables))
: browserOsDetector.detect(getUserAgent(variables));
}
this.isDesktop = isDesktop(variables);
if (this.isDesktop) {
this.os = getDesktopOsType(getProcessPlatform(variables));
} else {
const userAgent = getUserAgent(variables);
this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent);
}
}
}
function getUserAgent(variables: IEnvironmentVariables): string {
if (!variables.window || !variables.window.navigator) {
return undefined;
}
return variables.window.navigator.userAgent;
if (!variables.window || !variables.window.navigator) {
return undefined;
}
return variables.window.navigator.userAgent;
}
function getProcessPlatform(variables: IEnvironmentVariables): string {
if (!variables.process || !variables.process.platform) {
return undefined;
}
return variables.process.platform;
if (!variables.process || !variables.process.platform) {
return undefined;
}
return variables.process.platform;
}
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform
switch (processPlatform) {
case 'darwin':
return OperatingSystem.macOS;
case 'win32':
return OperatingSystem.Windows;
case 'linux':
return OperatingSystem.Linux;
default:
return undefined;
}
// https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') {
return OperatingSystem.macOS;
} else if (processPlatform === 'win32') {
return OperatingSystem.Windows;
} else if (processPlatform === 'linux') {
return OperatingSystem.Linux;
}
return undefined;
}
function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288
// Renderer process
if (variables.window
&& variables.window.process
&& variables.window.process.type === 'renderer') {
return true;
}
// Main process
if (variables.process
&& variables.process.versions
&& Boolean(variables.process.versions.electron)) {
return true;
}
// Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator
&& variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) {
return true;
}
return false;
// More: https://github.com/electron/electron/issues/2288
// Renderer process
if (variables.window
&& variables.window.process
&& variables.window.process.type === 'renderer') {
return true;
}
// Main process
if (variables.process
&& variables.process.versions
&& Boolean(variables.process.versions.electron)) {
return true;
}
// Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator
&& variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) {
return true;
}
return false;
}

View File

@@ -1,6 +1,6 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment {
readonly isDesktop: boolean;
readonly os: OperatingSystem;
readonly isDesktop: boolean;
readonly os: OperatingSystem;
}

View File

@@ -1,5 +1,5 @@
import { IApplication } from '@/domain/IApplication';
export interface IApplicationFactory {
getApp(): Promise<IApplication>;
getApp(): Promise<IApplication>;
}

View File

@@ -1,41 +1,38 @@
import { CollectionData } from 'js-yaml-loader!@/*';
import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { parseCategoryCollection } from './CategoryCollectionParser';
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication(
parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env,
collectionsData = PreParsedCollections,
): IApplication {
validateCollectionsData(collectionsData);
const information = parseProjectInformation(processEnv);
const collections = collectionsData.map((collection) => parser(collection, information));
const app = new Application(information, collections);
return app;
parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env,
collectionsData = PreParsedCollections): IApplication {
validateCollectionsData(collectionsData);
const information = parseProjectInformation(processEnv);
const collections = collectionsData.map((collection) => parser(collection, information));
const app = new Application(information, collections);
return app;
}
export type CategoryCollectionParserType
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
return parseCategoryCollection(file, info);
};
const CategoryCollectionParser: CategoryCollectionParserType
= (file, info) => parseCategoryCollection(file, info);
const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData,
];
const PreParsedCollections: readonly CollectionData []
= [ WindowsData, MacOsData ];
function validateCollectionsData(collections: readonly CollectionData[]) {
if (!collections.length) {
throw new Error('no collection provided');
}
if (collections.some((collection) => !collection)) {
throw new Error('undefined collection provided');
}
if (!collections.length) {
throw new Error('no collection provided');
}
if (collections.some((collection) => !collection)) {
throw new Error('undefined collection provided');
}
}

View File

@@ -1,37 +1,40 @@
import { Category } from '@/domain/Category';
import { CollectionData } from 'js-yaml-loader!@/*';
import { parseCategory } from './CategoryParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { createEnumParser } from '../Common/Enum';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { createEnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
export function parseCategoryCollection(
content: CollectionData,
info: IProjectInformation,
osParser = createEnumParser(OperatingSystem),
): ICategoryCollection {
validate(content);
const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info);
const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = content.actions.map((action) => parseCategory(action, context));
const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection(
os,
categories,
scripting,
);
return collection;
content: CollectionData,
info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
validate(content);
const scripting = new ScriptingDefinitionParser()
.parse(content.scripting, info);
const context = new CategoryCollectionParseContext(content.functions, scripting);
const categories = new Array<Category>();
for (const action of content.actions) {
const category = parseCategory(action, context);
categories.push(category);
}
const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection(
os,
categories,
scripting);
return collection;
}
function validate(content: CollectionData): void {
if (!content) {
throw new Error('content is null or undefined');
}
if (!content.actions || content.actions.length <= 0) {
throw new Error('content does not define any action');
}
if (!content) {
throw new Error('content is null or undefined');
}
if (!content.actions || content.actions.length <= 0) {
throw new Error('content does not define any action');
}
}

View File

@@ -1,86 +1,71 @@
import {
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
} from 'js-yaml-loader!@/*';
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser';
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
import { parseScript } from './Script/ScriptParser';
let categoryIdCounter = 0;
let categoryIdCounter: number = 0;
export function parseCategory(
category: CategoryData,
context: ICategoryCollectionParseContext,
): Category {
if (!context) { throw new Error('undefined context'); }
ensureValid(category);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
subScripts: new Array<Script>(),
};
for (const data of category.children) {
parseCategoryChild(data, children, category, context);
}
return new Category(
/* id: */ categoryIdCounter++,
/* name: */ category.category,
/* docs: */ parseDocUrls(category),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
if (!context) { throw new Error('undefined context'); }
ensureValid(category);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
subScripts: new Array<Script>(),
};
for (const data of category.children) {
parseCategoryChild(data, children, category, context);
}
return new Category(
/*id*/ categoryIdCounter++,
/*name*/ category.category,
/*docs*/ parseDocUrls(category),
/*categories*/ children.subCategories,
/*scripts*/ children.subScripts,
);
}
function ensureValid(category: CategoryData) {
if (!category) {
throw Error('category is null or undefined');
}
if (!category.children || category.children.length === 0) {
throw Error(`category has no children: "${category.category}"`);
}
if (!category.category || category.category.length === 0) {
throw Error('category has no name');
}
}
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
if (!category) {
throw Error('category is null or undefined');
}
if (!category.children || category.children.length === 0) {
throw Error(`category has no children: "${category.category}"`);
}
if (!category.category || category.category.length === 0) {
throw Error('category has no name');
}
}
function parseCategoryChild(
data: CategoryOrScriptData,
children: ICategoryChildren,
parent: CategoryData,
context: ICategoryCollectionParseContext,
) {
if (isCategory(data)) {
const subCategory = parseCategory(data as CategoryData, context);
children.subCategories.push(subCategory);
} else if (isScript(data)) {
const scriptData = data as ScriptData;
const script = parseScript(scriptData, context);
children.subScripts.push(script);
} else {
throw new Error(`Child element is neither a category or a script.
data: CategoryOrScriptData,
children: ICategoryChildren,
parent: CategoryData,
context: ICategoryCollectionParseContext) {
if (isCategory(data)) {
const subCategory = parseCategory(data as CategoryData, context);
children.subCategories.push(subCategory);
} else if (isScript(data)) {
const scriptData = data as ScriptData;
const script = parseScript(scriptData, context);
children.subScripts.push(script);
} else {
throw new Error(`Child element is neither a category or a script.
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
}
}
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
const holder = (data as InstructionHolder);
return hasCode(holder) || hasCall(holder);
function isScript(data: any): boolean {
return (data.code && data.code.length > 0)
|| data.call;
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
const { category } = data as CategoryData;
return category && category.length > 0;
}
function hasCode(holder: InstructionHolder): boolean {
return holder.code && holder.code.length > 0;
}
function hasCall(holder: InstructionHolder) {
return holder.call !== undefined;
function isCategory(data: any): boolean {
return data.category && data.category.length > 0;
}

View File

@@ -1,64 +1,61 @@
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
if (!documentable) {
throw new Error('documentable is null or undefined');
}
const { docs } = documentable;
if (!docs || !docs.length) {
return [];
}
let result = new DocumentationUrlContainer();
result = addDocs(docs, result);
return result.getAll();
if (!documentable) {
throw new Error('documentable is null or undefined');
}
const docs = documentable.docs;
if (!docs || !docs.length) {
return [];
}
let result = new DocumentationUrlContainer();
result = addDocs(docs, result);
return result.getAll();
}
function addDocs(
docs: DocumentationUrlsData,
urls: DocumentationUrlContainer,
): DocumentationUrlContainer {
if (docs instanceof Array) {
urls.addUrls(docs);
} else if (typeof docs === 'string') {
urls.addUrl(docs);
} else {
throw new Error('Docs field (documentation url) must a string or array of strings');
}
return urls;
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
if (docs instanceof Array) {
urls.addUrls(docs);
} else if (typeof docs === 'string') {
urls.addUrl(docs);
} else {
throw new Error('Docs field (documentation url) must a string or array of strings');
}
return urls;
}
class DocumentationUrlContainer {
private readonly urls = new Array<string>();
private readonly urls = new Array<string>();
public addUrl(url: string) {
validateUrl(url);
this.urls.push(url);
}
public addUrls(urls: readonly string[]) {
for (const url of urls) {
if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings');
}
this.addUrl(url);
public addUrl(url: string) {
validateUrl(url);
this.urls.push(url);
}
}
public getAll(): ReadonlyArray<string> {
return this.urls;
}
public addUrls(urls: readonly any[]) {
for (const url of urls) {
if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings');
}
this.addUrl(url);
}
}
public getAll(): ReadonlyArray<string> {
return this.urls;
}
}
function validateUrl(docUrl: string): void {
if (!docUrl) {
throw new Error('Documentation url is null or empty');
}
if (docUrl.includes('\n')) {
throw new Error('Documentation url cannot be multi-lined.');
}
const validUrlRegex = /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/g;
const res = docUrl.match(validUrlRegex);
if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`);
}
if (!docUrl) {
throw new Error('Documentation url is null or empty');
}
if (docUrl.includes('\n')) {
throw new Error('Documentation url cannot be multi-lined.');
}
const res = docUrl.match(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g);
if (res == null) {
throw new Error(`Invalid documentation url: ${docUrl}`);
}
}

View File

@@ -2,12 +2,11 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformation } from '@/domain/ProjectInformation';
export function parseProjectInformation(
environment: NodeJS.ProcessEnv,
): IProjectInformation {
return new ProjectInformation(
environment.VUE_APP_NAME,
environment.VUE_APP_VERSION,
environment.VUE_APP_REPOSITORY_URL,
environment.VUE_APP_HOMEPAGE_URL,
);
environment: NodeJS.ProcessEnv): IProjectInformation {
return new ProjectInformation(
environment.VUE_APP_NAME,
environment.VUE_APP_VERSION,
environment.VUE_APP_REPOSITORY_URL,
environment.VUE_APP_HOMEPAGE_URL,
);
}

View File

@@ -1,6 +1,6 @@
import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { FunctionData } from 'js-yaml-loader!@/*';
import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
@@ -8,17 +8,15 @@ import { SyntaxFactory } from './Syntax/SyntaxFactory';
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
public readonly compiler: IScriptCompiler;
public readonly compiler: IScriptCompiler;
public readonly syntax: ILanguageSyntax;
public readonly syntax: ILanguageSyntax;
constructor(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) {
if (!scripting) { throw new Error('undefined scripting'); }
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax);
}
constructor(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
if (!scripting) { throw new Error('undefined scripting'); }
this.syntax = syntaxFactory.create(scripting.language);
this.compiler = new ScriptCompiler(functionsData, this.syntax);
}
}

View File

@@ -1,63 +1,62 @@
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { ExpressionPosition } from './ExpressionPosition';
import { IExpression } from './IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { IExpression } from './IExpression';
import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression {
constructor(
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator,
public readonly parameters
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
) {
if (!position) {
throw new Error('undefined position');
constructor(
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator,
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
if (!position) {
throw new Error('undefined position');
}
if (!evaluator) {
throw new Error('undefined evaluator');
}
}
if (!evaluator) {
throw new Error('undefined evaluator');
public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('undefined context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args);
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(context);
}
}
public evaluate(context: IExpressionEvaluationContext): string {
if (!context) {
throw new Error('undefined context');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
const args = filterUnusedArguments(this.parameters, context.args);
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
return this.evaluator(filteredContext);
}
}
function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection,
parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection,
) {
const requiredParameterNames = parameters
.all
.filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name);
const missingParameterNames = requiredParameterNames
.filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) {
throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
);
}
const requiredParameterNames = parameters
.all
.filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name);
const missingParameterNames = requiredParameterNames
.filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) {
throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
}
}
function filterUnusedArguments(
parameters: IReadOnlyFunctionParameterCollection,
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection();
parameters.all
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
.forEach((argument) => specificCallArgs.addArgument(argument));
return specificCallArgs;
parameters: IReadOnlyFunctionParameterCollection,
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection();
for (const parameter of parameters.all) {
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
continue; // Optional parameter is not necessarily provided
}
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
}
return specificCallArgs;
}

View File

@@ -3,17 +3,16 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
export interface IExpressionEvaluationContext {
readonly args: IReadOnlyFunctionCallArgumentCollection;
readonly pipelineCompiler: IPipelineCompiler;
readonly args: IReadOnlyFunctionCallArgumentCollection;
readonly pipelineCompiler: IPipelineCompiler;
}
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
constructor(
public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
) {
if (!args) {
throw new Error('undefined args, send empty collection instead');
constructor(
public readonly args: IReadOnlyFunctionCallArgumentCollection,
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
}
}
}

View File

@@ -1,16 +1,15 @@
export class ExpressionPosition {
constructor(
public readonly start: number,
public readonly end: number,
) {
if (start === end) {
throw new Error(`no length (start = end = ${start})`);
constructor(
public readonly start: number,
public readonly end: number) {
if (start === end) {
throw new Error(`no length (start = end = ${start})`);
}
if (start > end) {
throw Error(`start (${start}) after end (${end})`);
}
if (start < 0) {
throw Error(`negative start position: ${start}`);
}
}
if (start > end) {
throw Error(`start (${start}) after end (${end})`);
}
if (start < 0) {
throw Error(`negative start position: ${start}`);
}
}
}

View File

@@ -1,9 +1,9 @@
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
export interface IExpression {
readonly position: ExpressionPosition;
readonly parameters: IReadOnlyFunctionParameterCollection;
evaluate(context: IExpressionEvaluationContext): string;
readonly position: ExpressionPosition;
readonly parameters: IReadOnlyFunctionParameterCollection;
evaluate(context: IExpressionEvaluationContext): string;
}

View File

@@ -1,85 +1,78 @@
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import { IExpressionsCompiler } from './IExpressionsCompiler';
import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
export class ExpressionsCompiler implements IExpressionsCompiler {
public constructor(
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
) { }
public compileExpressions(
code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection,
): string {
if (!args) {
throw new Error('undefined args, send empty collection instead');
public constructor(
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
public compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
const expressions = this.extractor.findExpressions(code);
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileExpressions(expressions, code, context);
return compiledCode;
}
if (!code) {
return code;
}
const expressions = this.extractor.findExpressions(code);
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const context = new ExpressionEvaluationContext(args);
const compiledCode = compileExpressions(expressions, code, context);
return compiledCode;
}
}
function compileExpressions(
expressions: readonly IExpression[],
code: string,
context: IExpressionEvaluationContext,
) {
let compiledCode = '';
const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start);
let index = 0;
while (index !== code.length) {
const nextExpression = sortedExpressions.pop();
if (nextExpression) {
compiledCode += code.substring(index, nextExpression.position.start);
const expressionCode = nextExpression.evaluate(context);
compiledCode += expressionCode;
index = nextExpression.position.end;
} else {
compiledCode += code.substring(index, code.length);
break;
expressions: readonly IExpression[],
code: string,
context: IExpressionEvaluationContext) {
let compiledCode = '';
const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start);
let index = 0;
while (index !== code.length) {
const nextExpression = sortedExpressions.pop();
if (nextExpression) {
compiledCode += code.substring(index, nextExpression.position.start);
const expressionCode = nextExpression.evaluate(context);
compiledCode += expressionCode;
index = nextExpression.position.end;
} else {
compiledCode += code.substring(index, code.length);
break;
}
}
}
return compiledCode;
return compiledCode;
}
function extractRequiredParameterNames(
expressions: readonly IExpression[],
): string[] {
return expressions
.map((e) => e.parameters.all
.filter((p) => !p.isOptional)
.map((p) => p.name))
.filter(Boolean) // Remove empty or undefined
.flat()
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
expressions: readonly IExpression[]): string[] {
const usedParameterNames = expressions
.map((e) => e.parameters.all
.filter((p) => !p.isOptional)
.map((p) => p.name))
.filter((p) => p)
.flat();
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
return uniqueParameterNames;
}
function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection,
): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
return;
}
const notProvidedParameters = usedParameterNames
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
if (notProvidedParameters.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
}
expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
return;
}
const notProvidedParameters = usedParameterNames
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
if (notProvidedParameters.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
}
}
function printList(list: readonly string[]): string {
return `"${list.join('", "')}"`;
return `"${list.join('", "')}"`;
}

View File

@@ -1,7 +1,7 @@
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
export interface IExpressionsCompiler {
compileExpressions(
code: string | undefined,
args: IReadOnlyFunctionCallArgumentCollection): string;
compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string;
}

View File

@@ -1,23 +1,27 @@
import { IExpression } from '../Expression/IExpression';
import { IExpressionParser } from './IExpressionParser';
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
import { WithParser } from '../SyntaxParsers/WithParser';
import { IExpressionParser } from './IExpressionParser';
const Parsers = [
new ParameterSubstitutionParser(),
new WithParser(),
new ParameterSubstitutionParser(),
new WithParser(),
];
export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (leafs.some((leaf) => !leaf)) {
throw new Error('undefined leaf');
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (leafs.some((leaf) => !leaf)) {
throw new Error('undefined leaf');
}
}
public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>();
for (const parser of this.leafs) {
const newExpressions = parser.findExpressions(code);
if (newExpressions && newExpressions.length) {
expressions.push(...newExpressions);
}
}
return expressions;
}
}
public findExpressions(code: string): IExpression[] {
return this.leafs.flatMap(
(parser) => parser.findExpressions(code) || [],
);
}
}

View File

@@ -1,5 +1,5 @@
import { IExpression } from '../Expression/IExpression';
export interface IExpressionParser {
findExpressions(code: string): IExpression[];
findExpressions(code: string): IExpression[];
}

View File

@@ -1,60 +1,59 @@
export class ExpressionRegexBuilder {
private readonly parts = new Array<string>();
private readonly parts = new Array<string>();
public expectCharacters(characters: string) {
return this.addRawRegex(
characters
.replaceAll('$', '\\$')
.replaceAll('.', '\\.'),
);
}
public expectCharacters(characters: string) {
return this.addRawRegex(
characters
.replaceAll('$', '\\$')
.replaceAll('.', '\\.'),
);
}
public expectOneOrMoreWhitespaces() {
return this
.addRawRegex('\\s+');
}
public expectOneOrMoreWhitespaces() {
return this
.addRawRegex('\\s+');
}
public matchPipeline() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
}
public matchPipeline() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(\\|\\s*.+?)?');
}
public matchUntilFirstWhitespace() {
return this
.addRawRegex('([^|\\s]+)');
}
public matchUntilFirstWhitespace() {
return this
.addRawRegex('([^|\\s]+)');
}
public matchAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)')
.expectZeroOrMoreWhitespaces();
}
public matchAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)')
.expectZeroOrMoreWhitespaces();
}
public expectExpressionStart() {
return this
.expectCharacters('{{')
.expectZeroOrMoreWhitespaces();
}
public expectExpressionStart() {
return this
.expectCharacters('{{')
.expectZeroOrMoreWhitespaces();
}
public expectExpressionEnd() {
return this
.expectZeroOrMoreWhitespaces()
.expectCharacters('}}');
}
public expectExpressionEnd() {
return this
.expectZeroOrMoreWhitespaces()
.expectCharacters('}}');
}
public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g');
}
public buildRegExp(): RegExp {
return new RegExp(this.parts.join(''), 'g');
}
private expectZeroOrMoreWhitespaces() {
return this
.addRawRegex('\\s*');
}
private addRawRegex(regex: string) {
this.parts.push(regex);
return this;
}
private expectZeroOrMoreWhitespaces() {
return this
.addRawRegex('\\s*');
}
private addRawRegex(regex: string) {
this.parts.push(regex);
return this;
}
}

View File

@@ -6,54 +6,43 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
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('undefined code');
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
const matches = code.matchAll(this.regex);
for (const match of matches) {
const primitiveExpression = this.buildExpression(match);
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
const parameters = createParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
const matches = Array.from(code.matchAll(this.regex));
for (const match of matches) {
const startPos = match.index;
const endPos = startPos + match[0].length;
let position: ExpressionPosition;
try {
position = new ExpressionPosition(startPos, endPos);
} catch (error) {
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
}
const primitiveExpression = this.buildExpression(match);
const parameters = getParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
}
}
}
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 createPosition(match: RegExpMatchArray): ExpressionPosition {
const startPos = match.index;
const endPos = startPos + match[0].length;
return new ExpressionPosition(startPos, endPos);
}
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[];
evaluator: ExpressionEvaluator;
parameters?: readonly IFunctionParameter[];
}
function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection {
const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter);
}
return parameters;
}

View File

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

View File

@@ -1,3 +1,3 @@
export interface IPipelineCompiler {
compile(value: string, pipeline: string): string;
compile(value: string, pipeline: string): string;
}

View File

@@ -1,30 +1,27 @@
import { IPipe } from '../IPipe';
export class EscapeDoubleQuotes implements IPipe {
public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
return raw?.replaceAll('"', '"^""');
/* eslint-disable max-len */
/*
"^"" is the most robust and stable choice.
Other options:
""
Breaks, because it is fundamentally unsupported
""""
Does not work with consecutive double quotes.
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
\"
May break as they are interpreted by cmd.exe as metacharacters breaking the command
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
\""
Normalizes interior whitespace
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
A good explanation: https://stackoverflow.com/a/31413730
*/
/* eslint-enable max-len */
}
public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string {
return raw?.replaceAll('"', '"^""');
/*
"^"" is the most robust and stable choice.
Other options:
""
Breaks, because it is fundamentally unsupported
""""
Does not work with consecutive double quotes.
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
\"
May break as they are interpreted by cmd.exe as metacharacters breaking the command
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
\""
Normalizes interior whitespace
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
A good explanation: https://stackoverflow.com/a/31413730
*/
}
}

View File

@@ -1,166 +1,155 @@
import { IPipe } from '../IPipe';
export class InlinePowerShell implements IPipe {
public readonly name: string = 'inlinePowerShell';
public apply(code: string): string {
if (!code || !hasLines(code)) {
return code;
public readonly name: string = 'inlinePowerShell';
public apply(code: string): string {
if (!code || !hasLines(code)) {
return code;
}
code = inlineComments(code);
code = mergeLinesWithBacktick(code);
code = mergeHereStrings(code);
const lines = getLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0);
return lines
.join('; ');
}
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
inlineComments,
mergeLinesWithBacktick,
mergeHereStrings,
mergeNewLines,
]).reduce((a, b) => (data) => b(a(data)));
const newCode = processor(code);
return newCode;
}
}
function hasLines(text: string) {
return text.includes('\n') || text.includes('\r');
return text.includes('\n') || text.includes('\r');
}
/*
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
Otherwise single # comments out rest of the code
*/
function inlineComments(code: string): string {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
if (!value) {
return '<##>';
}
return `<# ${value} #>`;
};
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
/*
Other alternatives considered:
--------------------------
/#(?<!<#)(?![<>])(.*)$/gm
-------------------------
✅ Simple, yet matches and captures only what's necessary
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
❌ Uses lookbehind
Safari does not yet support lookbehind and syntax, leading application to not
load and throw "Invalid regular expression: invalid group specifier name"
https://caniuse.com/js-regexp-lookbehind
⏩ Usage
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
return makeInlineComment(captureComment)
});
----------------
/<#.*?#>|#(.*)/g
----------------
✅ Simple yet affective
❌ Matches all comments, but only captures dash comments
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
⏩ Usage
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
const makeInlineComment = (comment: string) => {
const value = comment?.trim();
if (!value) {
return '<##>';
}
return `<# ${value} #>`;
};
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
------------------------------------
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
------------------------------------
✅ Covers all cases
❌ Matches every line, three capture groups are used to build result
⏩ Usage
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
(match, captureLeft, captureDash, captureComment) => {
if (!captureDash) {
return match;
}
return captureLeft + makeInlineComment(captureComment);
});
}
return makeInlineComment(captureComment);
});
/*
Other alternatives considered:
--------------------------
/#(?<!<#)(?![<>])(.*)$/gm
-------------------------
✅ Simple, yet matches and captures only what's necessary
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
❌ Uses lookbehind
Safari does not yet support lookbehind and syntax, leading application to not
load and throw "Invalid regular expression: invalid group specifier name"
https://caniuse.com/js-regexp-lookbehind
⏩ Usage
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
return makeInlineComment(captureComment)
});
----------------
/<#.*?#>|#(.*)/g
----------------
✅ Simple yet affective
❌ Matches all comments, but only captures dash comments
❌ Fails to match some cases
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
⏩ Usage
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
if (captureComment === undefined) {
return match;
}
return makeInlineComment(captureComment);
});
------------------------------------
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
------------------------------------
✅ Covers all cases
❌ Matches every line, three capture groups are used to build result
⏩ Usage
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
(match, captureLeft, captureDash, captureComment) => {
if (!captureDash) {
return match;
}
return captureLeft + makeInlineComment(captureComment);
});
*/
}
function getLines(code: string): string[] {
return (code?.split(/\r\n|\r|\n/) || []);
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)
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
*/
function mergeHereStrings(code: string) {
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = getLines(escaped);
const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted;
});
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = getLines(escaped);
const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted;
});
}
interface IInlinedHereString {
readonly quotesAround: string;
readonly escapedQuotes: string;
readonly separator: string;
readonly quotesAround: string;
readonly escapedQuotes: string;
readonly separator: string;
}
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
function getHereStringHandler(quotes: string): IInlinedHereString {
/*
We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable.
*/
const expandableNewLine = '`r`n';
switch (quotes) {
case '\'':
return {
quotesAround: '\'',
escapedQuotes: '\'\'',
separator: `'+"${expandableNewLine}"+'`,
};
case '"':
return {
quotesAround: '"',
escapedQuotes: '`"',
separator: expandableNewLine,
};
default:
throw new Error(`expected quotes: ${quotes}`);
}
const expandableNewLine = '`r`n';
switch (quotes) {
case '\'':
return {
quotesAround: '\'',
escapedQuotes: '\'\'',
separator: `\'+"${expandableNewLine}"+\'`,
};
case '"':
return {
quotesAround: '"',
escapedQuotes: '`"',
separator: expandableNewLine,
};
default:
throw new Error(`expected quotes: ${quotes}`);
}
}
/*
Input ->
Get-Service * `
Sort-Object StartType `
Format-Table Name, ServiceType, Status -AutoSize
Output ->
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
Input ->
Get-Service * `
Sort-Object StartType `
Format-Table Name, ServiceType, Status -AutoSize
Output ->
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
*/
function mergeLinesWithBacktick(code: string) {
/*
The regex actually wraps any whitespace character after backtick and before newline
However, this is not always the case for PowerShell.
I see two behaviors:
1. If inside string, it's accepted (inside " or ')
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
However we don't need to be so robust and handle this complexity (yet), so for easier regex
we wrap it anyway
*/
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
}
function mergeNewLines(code: string) {
return getLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('; ');
/*
The regex actually wraps any whitespace character after backtick and before newline
However, this is not always the case for PowerShell.
I see two behaviors:
1. If inside string, it's accepted (inside " or ')
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
However we don't need to be so robust and handle this complexity (yet), so for easier regex
we wrap it anyway
*/
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
}

View File

@@ -3,48 +3,45 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
const RegisteredPipes = [
new EscapeDoubleQuotes(),
new InlinePowerShell(),
new EscapeDoubleQuotes(),
new InlinePowerShell(),
];
export interface IPipeFactory {
get(pipeName: string): IPipe;
get(pipeName: string): IPipe;
}
export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) {
throw new Error('undefined pipe in list');
private readonly pipes = new Map<string, IPipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
if (pipes.some((pipe) => !pipe)) {
throw new Error('undefined pipe in list');
}
for (const pipe of pipes) {
this.registerPipe(pipe);
}
}
for (const pipe of pipes) {
this.registerPipe(pipe);
public get(pipeName: string): IPipe {
validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) {
throw new Error(`Unknown pipe: "${pipeName}"`);
}
return this.pipes.get(pipeName);
}
}
public get(pipeName: string): IPipe {
validatePipeName(pipeName);
if (!this.pipes.has(pipeName)) {
throw new Error(`Unknown pipe: "${pipeName}"`);
private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
}
this.pipes.set(pipe.name, pipe);
}
return this.pipes.get(pipeName);
}
private registerPipe(pipe: IPipe): void {
validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
}
this.pipes.set(pipe.name, pipe);
}
}
function validatePipeName(name: string) {
if (!name) {
throw new Error('empty pipe name');
}
if (!/^[a-z][A-Za-z]*$/.test(name)) {
throw new Error(`Pipe name should be camelCase: "${name}"`);
}
if (!name) {
throw new Error('empty pipe name');
}
if (!/^[a-z][A-Za-z]*$/.test(name)) {
throw new Error(`Pipe name should be camelCase: "${name}"`);
}
}

View File

@@ -2,30 +2,30 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
import { IPipelineCompiler } from './IPipelineCompiler';
export class PipelineCompiler implements IPipelineCompiler {
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
return pipes.reduce((previousValue, pipe) => {
return pipe.apply(previousValue);
}, value);
}
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
public compile(value: string, pipeline: string): string {
ensureValidArguments(value, pipeline);
const pipeNames = extractPipeNames(pipeline);
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
for (const pipe of pipes) {
value = pipe.apply(value);
}
return value;
}
}
function extractPipeNames(pipeline: string): string[] {
return pipeline
.trim()
.split('|')
.slice(1)
.map((p) => p.trim());
return pipeline
.trim()
.split('|')
.slice(1)
.map((p) => p.trim());
}
function ensureValidArguments(value: string, pipeline: string) {
if (!value) { throw new Error('undefined value'); }
if (!pipeline) { throw new Error('undefined pipeline'); }
if (!pipeline.trimStart().startsWith('|')) {
throw new Error('pipeline does not start with pipe');
}
if (!value) { throw new Error('undefined value'); }
if (!pipeline) { throw new Error('undefined pipeline'); }
if (!pipeline.trimStart().startsWith('|')) {
throw new Error('pipeline does not start with pipe');
}
}

View File

@@ -1,28 +1,28 @@
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline
.expectExpressionEnd()
.buildRegExp();
protected readonly regex = new ExpressionRegexBuilder()
.expectExpressionStart()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: Parameter name
.matchPipeline() // Second match: Pipeline
.expectExpressionEnd()
.buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const pipeline = match[2];
return {
parameters: [new FunctionParameter(parameterName, false)],
evaluator: (context) => {
const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
},
};
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const pipeline = match[2];
return {
parameters: [ new FunctionParameter(parameterName, false) ],
evaluator: (context) => {
const argumentValue = context.args.getArgument(parameterName).argumentValue;
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
},
};
}
}

View File

@@ -1,59 +1,58 @@
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class WithParser extends RegexParser {
protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd()
.buildRegExp();
protected readonly regex = new ExpressionRegexBuilder()
// {{ with $parameterName }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.matchUntilFirstWhitespace() // First match: parameter name
.expectExpressionEnd()
// ...
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
// {{ end }}
.expectExpressionStart()
.expectCharacters('end')
.expectExpressionEnd()
.buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
return {
parameters: [new FunctionParameter(parameterName, true)],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName)
? context.args.getArgument(parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
},
};
}
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
const scopeText = match[2];
return {
parameters: [ new FunctionParameter(parameterName, true) ],
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName) ?
context.args.getArgument(parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
});
},
};
}
}
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectExpressionEnd()
.buildRegExp();
// {{ . | pipeName }}
.expectExpressionStart()
.expectCharacters('.')
.matchPipeline() // First match: pipeline
.expectExpressionEnd()
.buildRegExp();
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
// but instead letting the pipeline compiler to fail on those.
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
return replacer(match1);
});
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
return replacer(match1);
});
}

View File

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

View File

@@ -2,37 +2,33 @@ import { IFunctionCallArgument } from './IFunctionCallArgument';
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('undefined argument');
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('undefined argument');
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
}
this.arguments.set(argument.parameterName, argument);
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys());
}
this.arguments.set(argument.parameterName, argument);
}
public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys());
}
public hasArgument(parameterName: string): boolean {
if (!parameterName) {
throw new Error('undefined parameter name');
public hasArgument(parameterName: string): boolean {
if (!parameterName) {
throw new Error('undefined parameter name');
}
return this.arguments.has(parameterName);
}
return this.arguments.has(parameterName);
}
public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) {
throw new Error('undefined parameter name');
public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) {
throw new Error('undefined parameter name');
}
const arg = this.arguments.get(parameterName);
if (!arg) {
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
}
const arg = this.arguments.get(parameterName);
if (!arg) {
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
}
}

View File

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

View File

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

View File

@@ -1,149 +1,139 @@
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler';
import { ISharedFunction, IFunctionCode } from '../../ISharedFunction';
import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall';
import { FunctionCall } from '../FunctionCall';
import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { ICompiledCode } from './ICompiledCode';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) {
}
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
}
public compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection,
): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); }
if (!calls) { throw new Error('undefined calls'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
const context: ICompilationContext = {
allFunctions: functions,
callSequence: calls,
expressionsCompiler: this.expressionsCompiler,
};
const code = compileCallSequence(context);
return code;
}
public compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); }
if (!calls) { throw new Error('undefined calls'); }
if (calls.some((f) => !f)) { throw new Error('undefined function call'); }
const context: ICompilationContext = {
allFunctions: functions,
callSequence: calls,
expressionsCompiler: this.expressionsCompiler,
};
const code = compileCallSequence(context);
return code;
}
}
interface ICompilationContext {
allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler;
allFunctions: ISharedFunctionCollection;
callSequence: readonly IFunctionCall[];
expressionsCompiler: IExpressionsCompiler;
}
interface ICompiledFunctionCall {
readonly code: string;
readonly revertCode: string;
readonly code: string;
readonly revertCode: string;
}
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
const compiledFunctions = context.callSequence
.flatMap((call) => compileSingleCall(call, context));
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
const compiledFunctions = new Array<ICompiledFunctionCall>();
for (const call of context.callSequence) {
const compiledCode = compileSingleCall(call, context);
compiledFunctions.push(...compiledCode);
}
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
}
function compileSingleCall(
call: IFunctionCall,
context: ICompilationContext,
): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [compiledCode];
}
// Function with inner calls
return func.body.calls
.map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] {
const func = context.allFunctions.getFunctionByName(call.functionName);
ensureThatCallArgumentsExistInParameterDefinition(func, call.args);
if (func.body.code) { // Function with inline code
const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler);
return [ compiledCode ];
} else { // Function with inner calls
return func.body.calls
.map((innerCall) => {
const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler);
const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs);
return compileSingleCall(compiledCall, context);
})
.flat();
}
}
function compileCode(
code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
code: IFunctionCode,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunctionCall {
return {
code: compiler.compileExpressions(code.do, args),
revertCode: compiler.compileExpressions(code.revert, args),
};
}
function compileArgs(
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
return argsToCompile
.getAllParameterNames()
.map((parameterName) => {
const { argumentValue } = argsToCompile.getArgument(parameterName);
const compiledValue = compiler.compileExpressions(argumentValue, args);
return new FunctionCallArgument(parameterName, compiledValue);
})
.reduce((compiledArgs, arg) => {
compiledArgs.addArgument(arg);
return compiledArgs;
}, new FunctionCallArgumentCollection());
argsToCompile: IReadOnlyFunctionCallArgumentCollection,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler,
): IReadOnlyFunctionCallArgumentCollection {
const compiledArgs = new FunctionCallArgumentCollection();
for (const parameterName of argsToCompile.getAllParameterNames()) {
const argumentValue = argsToCompile.getArgument(parameterName).argumentValue;
const compiledValue = compiler.compileExpressions(argumentValue, args);
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
compiledArgs.addArgument(newArgument);
}
return compiledArgs;
}
function merge(codeParts: readonly string[]): string {
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
}
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection,
): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames);
throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames);
}
function findUnexpectedParameters(
callArgumentNames: string[],
functionParameterNames: string[],
): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
callArgumentNames: string[],
functionParameterNames: string[]): string[] {
if (!callArgumentNames.length && !functionParameterNames.length) {
return [];
}
return callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
}
function throwIfNotEmpty(
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[],
) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
// eslint-disable-next-line prefer-template
`Function "${functionName}" has unexpected parameter(s) provided: `
+ `"${unexpectedParameters.join('", "')}"`
+ '. Expected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
functionName: string,
unexpectedParameters: string[],
expectedParameters: string[]) {
if (!unexpectedParameters.length) {
return;
}
throw new Error(
`Function "${functionName}" has unexpected parameter(s) provided: ` +
`"${unexpectedParameters.join('", "')}"` +
'. Expected parameter(s): ' +
(expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
);
}

View File

@@ -1,4 +1,4 @@
export interface ICompiledCode {
readonly code: string;
readonly revertCode?: string;
readonly code: string;
readonly revertCode?: string;
}

View File

@@ -1,9 +1,9 @@
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { IFunctionCall } from '../IFunctionCall';
import { ICompiledCode } from './ICompiledCode';
export interface IFunctionCallCompiler {
compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode;
compileCall(
calls: IFunctionCall[],
functions: ISharedFunctionCollection): ICompiledCode;
}

View File

@@ -2,15 +2,14 @@ import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCal
import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall {
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection,
) {
if (!functionName) {
throw new Error('empty function name in function call');
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
if (!functionName) {
throw new Error('empty function name in function call');
}
if (!args) {
throw new Error('undefined args');
}
}
if (!args) {
throw new Error('undefined args');
}
}
}

View File

@@ -1,42 +1,35 @@
import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*';
import { IFunctionCall } from './IFunctionCall';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { FunctionCall } from './FunctionCall';
export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] {
if (calls === undefined) {
throw new Error('undefined call data');
}
const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call));
if (!calls) {
throw new Error('undefined call data');
}
const sequence = getCallSequence(calls);
return sequence.map((call) => parseFunctionCall(call));
}
function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
if (typeof calls !== 'object') {
throw new Error('called function(s) must be an object');
}
if (calls instanceof Array) {
return calls as FunctionCallData[];
}
return [calls as FunctionCallData];
if (typeof calls !== 'object') {
throw new Error('called function(s) must be an object');
}
if (calls instanceof Array) {
return calls as FunctionCallData[];
}
return [ calls as FunctionCallData ];
}
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) {
throw new Error('undefined function call');
}
const callArgs = parseArgs(call.parameters);
return new FunctionCall(call.function, callArgs);
}
function parseArgs(
parameters: FunctionCallParametersData,
): FunctionCallArgumentCollection {
return Object.keys(parameters || {})
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
.reduce((args, arg) => {
args.addArgument(arg);
return args;
}, new FunctionCallArgumentCollection());
if (!call) {
throw new Error(`undefined function call`);
}
const args = new FunctionCallArgumentCollection();
for (const parameterName of Object.keys(call.parameters || {})) {
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
args.addArgument(arg);
}
return new FunctionCall(call.function, args);
}

View File

@@ -1,6 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -1,24 +1,24 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import { IFunctionCall } from './Call/IFunctionCall';
import { IFunctionCall } from '../Function/Call/IFunctionCall';
export interface ISharedFunction {
readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody;
readonly name: string;
readonly parameters: IReadOnlyFunctionParameterCollection;
readonly body: ISharedFunctionBody;
}
export interface ISharedFunctionBody {
readonly type: FunctionBodyType;
readonly code: IFunctionCode;
readonly calls: readonly IFunctionCall[];
readonly type: FunctionBodyType;
readonly code: IFunctionCode;
readonly calls: readonly IFunctionCall[];
}
export enum FunctionBodyType {
Code,
Calls,
Code,
Calls,
}
export interface IFunctionCode {
readonly do: string;
readonly revert?: string;
readonly do: string;
readonly revert?: string;
}

View File

@@ -1,5 +1,5 @@
import { ISharedFunction } from './ISharedFunction';
export interface ISharedFunctionCollection {
getFunctionByName(name: string): ISharedFunction;
getFunctionByName(name: string): ISharedFunction;
}

View File

@@ -2,5 +2,5 @@ import { FunctionData } from 'js-yaml-loader!@/*';
import { ISharedFunctionCollection } from './ISharedFunctionCollection';
export interface ISharedFunctionsParser {
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection;
}

View File

@@ -1,11 +1,10 @@
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../Shared/ParameterNameValidator';
export class FunctionParameter implements IFunctionParameter {
constructor(
public readonly name: string,
public readonly isOptional: boolean,
) {
ensureValidParameterName(name);
}
constructor(
public readonly name: string,
public readonly isOptional: boolean) {
ensureValidParameterName(name);
}
}

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